diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b21dd53e5..7c0e7e5c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,12 +3,15 @@ updates: - package-ecosystem: github-actions directory: "/" schedule: - interval: daily + interval: monthly timezone: America/New_York open-pull-requests-limit: 10 - package-ecosystem: cargo directory: "/" + # Update only the lockfile. We shouldn't update Cargo.toml unless it's for + # a security issue, or if we need a new feature of the dependency. + versioning-strategy: lockfile-only schedule: - interval: daily + interval: monthly timezone: America/New_York open-pull-requests-limit: 10 diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 47ece33b1..55ce03720 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -23,25 +23,22 @@ jobs: RUST_BACKTRACE: full steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 with: persist-credentials: false - - uses: actions-rs/toolchain@v1.0.7 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true - profile: minimal - components: llvm-tools-preview + components: llvm-tools - name: Install cargo-llvm-cov cargo command run: cargo install cargo-llvm-cov - name: Run tests - run: cargo llvm-cov --lcov --no-report --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' + run: cargo llvm-cov --lcov --no-report --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs|interoperability_tests.rs' - name: Generate coverage report - run: cargo llvm-cov report --lcov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' --output-path lcov.info + run: cargo llvm-cov report --lcov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs|interoperability_tests.rs' --output-path lcov.info - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v6.0.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 11dbc6710..da14f626c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,14 +15,20 @@ on: paths: # doc source files - 'book/**' - - '**/firebase.json' - - 'katex-header.html' + # source files; some md files include code snippets that we want to keep up to date + - 'frost-*/**' # workflow definitions - '.github/workflows/docs.yml' push: branches: - main +# Sets permissions for GitHub Pages deployment +permissions: + contents: read + pages: write + id-token: write + env: RUST_LOG: info RUST_BACKTRACE: full @@ -31,26 +37,22 @@ env: jobs: build: - name: Build and Deploy Docs (+beta) + name: Build Docs (+beta) timeout-minutes: 45 runs-on: ubuntu-latest steps: - name: Checkout the source code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v6.0.2 with: persist-credentials: false - name: Install latest beta - uses: actions-rs/toolchain@v1 - with: - toolchain: beta - components: rust-docs - override: true + uses: dtolnay/rust-toolchain@beta - uses: Swatinem/rust-cache@v2 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1.2.0 + uses: peaceiris/actions-mdbook@v2.0.0 with: mdbook-version: '0.4.18' @@ -64,23 +66,23 @@ jobs: run: | mdbook build book/ - - name: Deploy FROST book to Firebase preview channel - uses: FirebaseExtended/action-hosting-deploy@v0 - if: ${{ github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }} - with: - entrypoint: "book/" - expires: 14d - firebaseServiceAccount: ${{ secrets.GCP_SA_KEY }} - repoToken: ${{ secrets.GITHUB_TOKEN }} - projectId: ${{ vars.FIREBASE_PROJECT_ID }} - - - name: Deploy FROST book to Firebase live channel - uses: FirebaseExtended/action-hosting-deploy@v0 + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} with: - channelId: live - entrypoint: "book/" - firebaseServiceAccount: ${{ secrets.GCP_SA_KEY }} - repoToken: ${{ secrets.GITHUB_TOKEN }} - projectId: ${{ vars.FIREBASE_PROJECT_ID }} + path: 'book/book' + + deploy: + name: Deploy to GitHub Pages + if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + needs: build + timeout-minutes: 10 + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e547fcd19..c263aa9fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,49 +9,100 @@ on: - main jobs: - build_default: name: build with default features runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions-rs/toolchain@v1.0.7 - with: - toolchain: beta - override: true - - uses: actions-rs/cargo@v1.0.3 - with: - command: build + - uses: actions/checkout@v6.0.2 + - uses: dtolnay/rust-toolchain@beta + - run: cargo build + + build_latest: + name: build with latest versions of dependencies + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.2 + - uses: dtolnay/rust-toolchain@stable + - run: cargo update && cargo build --all-features + + build_msrv: + name: build with MSRV (1.81) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.2 + # Re-resolve Cargo.lock with minimal versions. + # This only works with nightly. We pin to a specific version because + # newer versions use lock file version 4, but the MSRV cargo does not + # support that. + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2024-09-20 + - run: cargo update -Z minimal-versions + # Now check that `cargo build` works with respect to the oldest possible + # deps and the stated MSRV + - uses: dtolnay/rust-toolchain@1.81 + - run: cargo build --all-features + + # TODO: this is filling up the disk space in CI. See if there is a way to + # workaround it. + + # build_all_features: + # name: build all features combinations + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v6.0.2 + # - uses: dtolnay/rust-toolchain@stable + # - run: cargo install cargo-all-features + # # We check and then test because some test dependencies could help + # # a bugged build work, while a regular build would fail. + # # Note that this also builds each crate separately, which also helps + # # catching some issues. + # - run: cargo check-all-features + # # Build all tests. We don't run them to save time, since it's unlikely + # # that tests would fail due to feature combinations. + # - run: cargo test-all-features --no-run + + build_no_std: + name: build with no_std + runs-on: ubuntu-latest + # Skip ed448 which does not support it. + strategy: + matrix: + crate: [ristretto255, ed25519, p256, secp256k1, secp256k1-tr, rerandomized] + steps: + - uses: actions/checkout@v6.0.2 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: thumbv6m-none-eabi + - run: cargo build -p frost-${{ matrix.crate }} --no-default-features --target thumbv6m-none-eabi + - run: cargo build -p frost-${{ matrix.crate }} --no-default-features --features serialization --target thumbv6m-none-eabi test_beta: name: test on beta runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions-rs/toolchain@v1.0.7 - with: - toolchain: beta - override: true - - uses: actions-rs/cargo@v1.0.3 - with: - command: test - args: --release --all-features + - uses: actions/checkout@v6.0.2 + - uses: dtolnay/rust-toolchain@beta + - run: cargo test --release --all-features clippy: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 with: persist-credentials: false - - uses: actions-rs/toolchain@v1.0.7 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true + components: clippy - name: Check workflow permissions id: check_permissions @@ -62,12 +113,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run clippy action to produce annotations - uses: actions-rs/clippy-check@v1.0.7 + uses: clechasseur/rs-clippy-check@v5 if: ${{ steps.check_permissions.outputs.has-permission }} with: - # GitHub displays the clippy job and its results as separate entries - name: Clippy (stable) Results - token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --all-targets -- -D warnings - name: Run clippy manually without annotations @@ -79,71 +127,56 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 with: persist-credentials: false - - uses: actions-rs/toolchain@v1.0.7 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt - override: true - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1.0.3 - with: - command: fmt - args: --all -- --check + - run: cargo fmt --all -- --check gencode: name: Check if automatically generated code is up to date runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 with: persist-credentials: false - - uses: actions-rs/toolchain@v1.0.7 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt - override: true - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1.0.3 - with: - command: run - args: --bin gencode -- --check + - run: cargo run --bin gencode -- --check docs: name: Check Rust doc runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: -D warnings steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 with: persist-credentials: false - - uses: actions-rs/toolchain@v1.0.7 - with: - toolchain: stable - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@stable - - uses: actions-rs/cargo@v1.0.3 - with: - command: doc - args: --no-deps --document-private-items --all-features + - run: cargo doc --no-deps --document-private-items --all-features actionlint: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4.1.1 - - uses: reviewdog/action-actionlint@v1.41.0 + - uses: actions/checkout@v6.0.2 + - uses: reviewdog/action-actionlint@v1.72.0 with: level: warning - fail_on_error: false + fail_level: none diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a110f6d82..23952d0c8 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into main - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@v7 with: # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml config-name: release-drafter.yml diff --git a/.gitignore b/.gitignore index 5d3d773b8..bbb076267 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /target **/*.rs.bk -Cargo.lock *~ **/.DS_Store .vscode/* diff --git a/.mergify.yml b/.mergify.yml index dff68c01c..7a64a0f3d 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,28 +1,32 @@ queue_rules: - name: main - allow_inplace_checks: True - allow_checks_interruption: True - speculative_checks: 1 - batch_size: 2 - # Wait for a few minutes to embark 2 tickets together in a merge train - batch_max_wait_time: "3 minutes" - conditions: + queue_conditions: + - base=main + - -draft + - label!=do-not-merge + merge_conditions: # Mergify automatically applies status check, approval, and conversation rules, # which are the same as the GitHub main branch protection rules # https://docs.mergify.com/conditions/#about-branch-protection - base=main + batch_size: 2 + # Wait for a few minutes to embark 2 tickets together in a merge train + batch_max_wait_time: "3 minutes" + merge_method: squash pull_request_rules: - name: main queue triggered when CI passes with 1 review + conditions: [] + actions: + queue: + +priority_rules: + - name: Priority rule from queue `main` conditions: - # This queue handles a PR if: - # - it targets main - # - is not in draft - # including automated dependabot PRs. - base=main - -draft - label!=do-not-merge - actions: - queue: - name: main - method: squash \ No newline at end of file + allow_checks_interruption: true + +merge_queue: + max_parallel_checks: 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..5ab53de20 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +* [Running and Debugging](#running-and-debugging) +* [Bug Reports](#bug-reports) +* [Pull Requests](#pull-requests) + +## Running and Debugging +[running-and-debugging]: #running-and-debugging + +See the [user documentation](https://frost.zfnd.org/user.html) for details on +how to build, run, and use the FROST Rust reference implementation. + +## Bug Reports +[bug-reports]: #bug-reports + +Please [create an issue](https://github.com/ZcashFoundation/frost/issues/new) on the FROST issue tracker. + +## Pull Requests +[pull-requests]: #pull-requests + +PRs are welcome for small and large changes, but please don't make large PRs +without coordinating with us via the [issue tracker](https://github.com/ZcashFoundation/frost/issues) or [Discord](https://discord.gg/muKwd2F83D). This helps +increase development coordination and makes PRs easier to merge. Low-effort PRs, including but not limited to fixing typos and grammatical corrections, will generally be redone by us to dissuade metric farming. + +Check out the [help wanted][hw] label if you're looking for a place to get started! + +FROST follows the [conventional commits][conventional] standard for the commits +merged to main. Since PRs are squashed before merging to main, the PR titles +should follow the conventional commits standard so that the merged commits +are conformant. + +[hw]: https://github.com/ZcashFoundation/frost/labels/E-help-wanted +[conventional]: https://www.conventionalcommits.org/en/v1.0.0/#specification \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..9f692371d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1676 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys", +] + +[[package]] +name = "const-crc32-nostd" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto 0.2.9", + "rand_core 0.6.4", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "debugless-unwrap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93fdcfc175d53094fca1d23d171685621bb67f1658413514f2053d575b3b38c" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "ed448-goldilocks" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88322282bccdc6fa7ab65b0c30cb877fba541547653436d08bb775fa4a4307b4" +dependencies = [ + "fiat-crypto 0.1.20", + "hex", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "frost-core" +version = "3.0.0-rc.0" +dependencies = [ + "byteorder", + "const-crc32-nostd", + "criterion", + "debugless-unwrap", + "derive-getters", + "document-features", + "hex", + "itertools 0.14.0", + "lazy_static", + "postcard", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_json", + "serdect", + "thiserror", + "tokio", + "visibility", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "frost-ed25519" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "curve25519-dalek", + "document-features", + "ed25519-dalek", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "lazy_static", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "frost-ed448" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "document-features", + "ed448-goldilocks", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "lazy_static", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde_json", + "sha3", + "tokio", +] + +[[package]] +name = "frost-p256" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "document-features", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "lazy_static", + "p256", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "frost-rerandomized" +version = "3.0.0-rc.0" +dependencies = [ + "derive-getters", + "document-features", + "frost-core", + "hex", + "rand_core 0.6.4", +] + +[[package]] +name = "frost-ristretto255" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "curve25519-dalek", + "document-features", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "lazy_static", + "postcard", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "frost-secp256k1" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "document-features", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "k256", + "lazy_static", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "frost-secp256k1-tr" +version = "3.0.0-rc.0" +dependencies = [ + "criterion", + "document-features", + "frost-core", + "frost-rerandomized", + "hex", + "insta", + "k256", + "lazy_static", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "secp256k1", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "gencode" +version = "0.1.0" +dependencies = [ + "regex", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "insta" +version = "1.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.2", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/Cargo.toml b/Cargo.toml index cbf054ff8..9ed7aacfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,44 @@ members = [ "frost-p256", "frost-ristretto255", "frost-secp256k1", + "frost-secp256k1-tr", "frost-rerandomized", "gencode" ] + +[workspace.package] +# If you update the edition, make sure to also update the argument to rustfmt +# in gencode/src/main.rs. +edition = "2021" +rust-version = "1.81" +version = "3.0.0-rc.0" +authors = [ + "Deirdre Connolly ", + "Chelsea Komlo ", + "Conrado Gouvea ", + "Natalie Eskinazi " +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/ZcashFoundation/frost" +categories = ["cryptography"] + +[workspace.dependencies] +# Currently holding back from updating due to MSRV 1.86 of criterion 0.8 +# (we don't want to raise our MSRV just for this) +criterion = "0.6" +document-features = "0.2.7" +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +insta = { version = "1.31.0", features = ["yaml"] } +lazy_static = "1.4" +proptest = "1.0" +rand = "0.8" +rand_chacha = "0.3" +rand_core = "0.6" +serde_json = "1.0" +tokio = { version = "1.0", features = ["rt", "time", "macros"] } + +frost-core = { path = "frost-core", version = "3.0.0-rc.0", default-features = false } +frost-rerandomized = { path = "frost-rerandomized", version = "3.0.0-rc.0", default-features = false } + +[profile.test.package."*"] +opt-level = 3 diff --git a/README.md b/README.md index 0101e8370..02948166a 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ [![CI](https://github.com/ZcashFoundation/frost/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/ZcashFoundation/frost/actions/workflows/main.yml) -| Crate | | Crates.io | Documentation | -| ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| Generic FROST implementation | [`frost-core`] | [![crates.io](https://img.shields.io/crates/v/frost-core.svg)](https://crates.io/crates/frost-core) | [![Documentation](https://docs.rs/frost-core/badge.svg)](https://docs.rs/frost-core) | -| Ristretto255 ciphersuite | [`frost-ristretto255`] | [![crates.io](https://img.shields.io/crates/v/frost-ristretto255.svg)](https://crates.io/crates/frost-ristretto255) | [![Documentation](https://docs.rs/frost-ristretto255/badge.svg)](https://docs.rs/frost-ristretto255) | -| Ed25519 ciphersuite | [`frost-ed25519`] | [![crates.io](https://img.shields.io/crates/v/frost-ed25519.svg)](https://crates.io/crates/frost-ed25519) | [![Documentation](https://docs.rs/frost-ed25519/badge.svg)](https://docs.rs/frost-ed25519) | -| Ed448 ciphersuite | [`frost-ed448`] | [![crates.io](https://img.shields.io/crates/v/frost-ed448.svg)](https://crates.io/crates/frost-ed448) | [![Documentation](https://docs.rs/frost-ed448/badge.svg)](https://docs.rs/frost-ed448) | -| P-256 ciphersuite | [`frost-p256`] | [![crates.io](https://img.shields.io/crates/v/frost-p256.svg)](https://crates.io/crates/frost-p256) | [![Documentation](https://docs.rs/frost-p256/badge.svg)](https://docs.rs/frost-p256) | -| secp256k1 ciphersuite | [`frost-secp256k1`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1.svg)](https://crates.io/crates/frost-secp256k1) | [![Documentation](https://docs.rs/frost-secp256k1/badge.svg)](https://docs.rs/frost-secp256k1) | -| Generic Re-randomized FROST | [`frost-rerandomized`] | [![crates.io](https://img.shields.io/crates/v/frost-rerandomized.svg)](https://crates.io/crates/frost-rerandomized) | [![Documentation](https://docs.rs/frost-rerandomized/badge.svg)](https://docs.rs/frost-rerandomized) | - -Rust implementations of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/). +| Crate | | Crates.io | Documentation | +| ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Generic FROST implementation | [`frost-core`] | [![crates.io](https://img.shields.io/crates/v/frost-core.svg)](https://crates.io/crates/frost-core) | [![Documentation](https://docs.rs/frost-core/badge.svg)](https://docs.rs/frost-core) | +| Ristretto255 ciphersuite | [`frost-ristretto255`] | [![crates.io](https://img.shields.io/crates/v/frost-ristretto255.svg)](https://crates.io/crates/frost-ristretto255) | [![Documentation](https://docs.rs/frost-ristretto255/badge.svg)](https://docs.rs/frost-ristretto255) | +| Ed25519 ciphersuite | [`frost-ed25519`] | [![crates.io](https://img.shields.io/crates/v/frost-ed25519.svg)](https://crates.io/crates/frost-ed25519) | [![Documentation](https://docs.rs/frost-ed25519/badge.svg)](https://docs.rs/frost-ed25519) | +| Ed448 ciphersuite | [`frost-ed448`] | [![crates.io](https://img.shields.io/crates/v/frost-ed448.svg)](https://crates.io/crates/frost-ed448) | [![Documentation](https://docs.rs/frost-ed448/badge.svg)](https://docs.rs/frost-ed448) | +| P-256 ciphersuite | [`frost-p256`] | [![crates.io](https://img.shields.io/crates/v/frost-p256.svg)](https://crates.io/crates/frost-p256) | [![Documentation](https://docs.rs/frost-p256/badge.svg)](https://docs.rs/frost-p256) | +| secp256k1 ciphersuite | [`frost-secp256k1`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1.svg)](https://crates.io/crates/frost-secp256k1) | [![Documentation](https://docs.rs/frost-secp256k1/badge.svg)](https://docs.rs/frost-secp256k1) | +| secp256k1 ciphersuite (Taproot) | [`frost-secp256k1-tr`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1-tr.svg)](https://crates.io/crates/frost-secp256k1-tr) | [![Documentation](https://docs.rs/frost-secp256k1-tr/badge.svg)](https://docs.rs/frost-secp256k1-tr) | +| Generic Re-randomized FROST | [`frost-rerandomized`] | [![crates.io](https://img.shields.io/crates/v/frost-rerandomized.svg)](https://crates.io/crates/frost-rerandomized) | [![Documentation](https://docs.rs/frost-rerandomized/badge.svg)](https://docs.rs/frost-rerandomized) | + +Rust implementations of ['RFC 9591: Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/rfc9591/). Unlike signatures in a single-party setting, threshold signatures require cooperation among a threshold number of signers, each holding a share of a common private key. The security of threshold @@ -20,7 +21,7 @@ schemes in general assume that an adversary can corrupt strictly fewer than a th participants. ['Two-Round Threshold Schnorr Signatures with -FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) presents a variant of a Flexible +FROST'](https://datatracker.ietf.org/doc/rfc9591/) presents a variant of a Flexible Round-Optimized Schnorr Threshold (FROST) signature scheme originally defined in [FROST20](https://eprint.iacr.org/2020/852.pdf). FROST reduces network overhead during threshold signing operations while employing a novel technique to protect against forgery attacks applicable @@ -28,21 +29,29 @@ to prior Schnorr-based threshold signature constructions. Besides FROST itself, this repository also provides: -- Trusted dealer key generation as specified in the appendix of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/); +- Trusted dealer key generation as specified in the appendix of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/rfc9591/); - Distributed key generation as specified in the original paper [FROST20](https://eprint.iacr.org/2020/852.pdf); -- Repairable Theshold Scheme (RTS) from ['A Survey and Refinement of Repairable Threshold Schemes'](https://eprint.iacr.org/2017/1155) which allows a participant to recover a lost share with the help of a threshold of other participants; -- Rerandomized FROST (paper under review). +- Repairable Threshold Scheme (RTS) from ['A Survey and Refinement of Repairable Threshold Schemes'](https://eprint.iacr.org/2017/1155) which allows a participant to recover a lost share with the help of a threshold of other participants; +- [Re-Randomized FROST](https://eprint.iacr.org/2024/436). +- Refresh Share functionality using a Trusted Dealer or Distributed Key + Generation. This can be used to refresh the shares of the participants or to + remove a participant. ## Getting Started -Refer to the [ZF FROST book](https://frost.zfnd.org/). +If you're not familiar with FROST, first read [Understanding FROST](frost.md). -## Status ⚠ +Then read the [Tutorial](tutorial.md), and use the [Rust docs](user.md) as +reference. -The FROST specification is not yet finalized, though no significant changes are -expected at this point. This code base has been partially audited by NCC, see -below for details. The APIs and types in the crates contained in this repository -follow SemVer guarantees. +## Status + +The crates are considered stable and feature complete, though eventual API +cleanups and additional functionality might be included in future releases. + +This code base has been partially audited by NCC, see below for details. The +APIs and types in the crates contained in this repository follow SemVer +guarantees. ### NCC Audit @@ -58,7 +67,7 @@ of the v0.6.0 release (corresponding to commit 5fa17ed) of the following crates: - frost-ristretto255 This includes key generation (both trusted dealer and DKG) and FROST signing. -This does not include rerandomized FROST. +This does not include frost-secp256k1-tr and rerandomized FROST. The parts of the [`Ed448-Goldilocks`](https://github.com/crate-crypto/Ed448-Goldilocks) @@ -72,7 +81,7 @@ All issues identified in the audit were addressed by us and reviewed by NCC. `frost-core` implements the base traits and types in a generic manner, to enable top-level implementations for different ciphersuites / curves without having to implement all of FROST from -scratch. End-users should not use `frost-core` if they want to sign and verify signatures, they +scratch. End-users should not use `frost-core` if they want to just sign and verify signatures for a specific ciphersuite; they should use the crate specific to their ciphersuite/curve parameters that uses `frost-core` as a dependency. diff --git a/book/book.toml b/book/book.toml index 5fdedd32f..e95c664fb 100644 --- a/book/book.toml +++ b/book/book.toml @@ -9,9 +9,9 @@ title = "The ZF FROST Book" [preprocessor.admonish] command = "mdbook-admonish" -assets_version = "3.0.0" # do not edit: managed by `mdbook-admonish install` +assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install` [output] [output.html] -additional-css = ["./mdbook-admonish.css"] +additional-css = ["./mdbook-admonish.css", "book/mdbook-admonish.css"] diff --git a/book/mdbook-admonish.css b/book/mdbook-admonish.css index e0a336553..45aeff051 100644 --- a/book/mdbook-admonish.css +++ b/book/mdbook-admonish.css @@ -1,20 +1,4 @@ @charset "UTF-8"; -:root { - --md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,"); - --md-details-icon: url("data:image/svg+xml;charset=utf-8,"); -} - :is(.admonition) { display: flow-root; margin: 1.5625em 0; @@ -71,6 +55,8 @@ a.admonition-anchor-link::before { padding-inline: 4.4rem 1.2rem; font-weight: 700; background-color: rgba(68, 138, 255, 0.1); + print-color-adjust: exact; + -webkit-print-color-adjust: exact; display: flex; } :is(.admonition-title, summary.admonition-title) p { @@ -86,6 +72,8 @@ html :is(.admonition-title, summary.admonition-title):last-child { width: 2rem; height: 2rem; background-color: #448aff; + print-color-adjust: exact; + -webkit-print-color-adjust: exact; mask-image: url('data:image/svg+xml;charset=utf-8,'); -webkit-mask-image: url('data:image/svg+xml;charset=utf-8,'); mask-repeat: no-repeat; @@ -119,6 +107,25 @@ details[open].admonition > summary.admonition-title::after { transform: rotate(90deg); } +:root { + --md-details-icon: url("data:image/svg+xml;charset=utf-8,"); +} + +:root { + --md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,"); +} + :is(.admonition):is(.admonish-note) { border-color: #448aff; } diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 4cf983c78..4ffa00e4c 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -7,11 +7,15 @@ - [Trusted Dealer Key Generation](tutorial/trusted-dealer.md) - [Signing](tutorial/signing.md) - [Distributed Key Generation](tutorial/dkg.md) + - [Refreshing Shares](tutorial/refreshing-shares.md) - [User Documentation](user.md) + - [Zeroization](user/zeroization.md) - [Serialization Format](user/serialization.md) - [FROST with Zcash](zcash.md) - [Technical Details](zcash/technical-details.md) + - [zcash-devtool Demo](zcash/devtool-demo.md) - [Ywallet Demo](zcash/ywallet-demo.md) + - [FROST Server](zcash/server.md) - [Terminology](terminology.md) - [Developer Documentation](dev.md) - [List of Dependencies for Audit](dev/frost-dependencies-for-audit.md) diff --git a/book/src/dev/developer-guide.md b/book/src/dev/developer-guide.md index a1384c622..7c31eec67 100644 --- a/book/src/dev/developer-guide.md +++ b/book/src/dev/developer-guide.md @@ -8,7 +8,7 @@ # Coverage -Test coverage checks are performed in the pipeline. This is cofigured here: `.github/workflows/coverage.yaml` +Test coverage checks are performed in the pipeline. This is configured here: `.github/workflows/coverage.yaml` To run these locally: 1. Install coverage tool by running `cargo install cargo-llvm-cov` -2. Run `cargo llvm-cov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs'` (you may be asked if you want to install `llvm-tools-preview`, if so type `Y`) +2. Run `cargo llvm-cov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs'` (you may be asked if you want to install `llvm-tools`, if so type `Y`) diff --git a/book/src/dev/frost-dependencies-for-audit.md b/book/src/dev/frost-dependencies-for-audit.md index 95227aaf2..98f8853c5 100644 --- a/book/src/dev/frost-dependencies-for-audit.md +++ b/book/src/dev/frost-dependencies-for-audit.md @@ -20,8 +20,8 @@ This is a list of production Rust code that is in scope and out of scope for FRO | Name | Version | Notes |------| ------- | ----- -| redjubjub | v0.6.0 | This library is being partially audited as part of the [Zebra audit](https://github.com/ZcashFoundation/zebra-private/blob/d4137908385be7e6df0a935b91bfc83b532261a2/book/src/dev/zebra-dependencies-for-audit.md#zcashzf-dependencies-1). -| reddsa | v0.5.0 | This library is being partially audited as part of the [Zebra audit](https://github.com/ZcashFoundation/zebra-private/blob/d4137908385be7e6df0a935b91bfc83b532261a2/book/src/dev/zebra-dependencies-for-audit.md#zcashzf-dependencies-1). +| redjubjub | v0.6.0 | This library is being partially audited as part of the [Zebra audit](https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/zebra-dependencies-for-audit.md#zcashzf-dependencies-1). +| reddsa | v0.5.0 | This library is being partially audited as part of the [Zebra audit](https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/zebra-dependencies-for-audit.md#zcashzf-dependencies-1). --- ## Partial Audit @@ -46,6 +46,7 @@ The following crates and dependencies are out of scope for the audit. | Name | Version | Notes |------| ------- | ----- | frost-rerandomized | v0.2.0 | To be audited after the security proof is complete. +| frost-secp256k1-tr | N/A | frost-secp256k1 with Taproot support, has not been audited yet. ### `frost-core` Dependencies diff --git a/book/src/dev/release-checklist.md b/book/src/dev/release-checklist.md index d4ba809f3..5005edf01 100644 --- a/book/src/dev/release-checklist.md +++ b/book/src/dev/release-checklist.md @@ -1,107 +1,114 @@ # Release Checklist +## One-time `gh` setup + +Install the [GitHub command line +tool](https://github.com/cli/cli?tab=readme-ov-file#installation) to make +releases easier. ## One-time crates.io setup -1. Follow the steps in (you can create a token scoped to `publish-update`). -2. To get permissions to publish you’ll need to be in the [owners](https://github.com/orgs/ZcashFoundation/teams/owners) group. If you aren’t in there, ask someone in that group to add you +- Follow the steps in (you can create a token scoped to `publish-update`). +- To get permissions to publish you’ll need to be in the [owners](https://github.com/orgs/ZcashFoundation/teams/owners) group. If you aren’t in there, ask someone in that group to add you ## Communication -3. Post in #frost slack channel and tag the Frost team that you’re going to be doing a release to freeze PR approvals until it’s done. E.g “@frost-core I’m doing a release of \ of Frost. Please do not merge any more PRs until I’m finished. Thanks.” +- Post in #frost slack channel and tag the Frost team that you’re going to be doing a release to freeze PR approvals until it’s done. E.g “@frost-core I’m doing a release of \ of Frost. Please do not merge any more PRs until I’m finished. Thanks.” ## Checks -4. Check current version for each crate. This is in the Cargo.toml file for frost-core, frost-ed448 etc. - - 1. [Frost core version number](https://github.com/ZcashFoundation/frost/blob/main/frost-core/Cargo.toml#L7) - 2. [Frost ed25519 version number](https://github.com/ZcashFoundation/frost/blob/main/frost-ed25519/Cargo.toml#L8) - 3. [Frost ed448 version number](https://github.com/ZcashFoundation/frost/blob/main/frost-ed448/Cargo.toml#L7) - 4. [Frost p256 version number](https://github.com/ZcashFoundation/frost/blob/main/frost-p256/Cargo.toml#L8) - 5. [Frost re randomized version number](https://github.com/ZcashFoundation/frost/blob/main/frost-rerandomized/Cargo.toml#L8) - 6. [Frost ristretto255 version number](https://github.com/ZcashFoundation/frost/blob/main/frost-ristretto255/Cargo.toml#L8) - 7. [Frost secp256k1 version number](https://github.com/ZcashFoundation/frost/blob/main/frost-secp256k1/Cargo.toml#L7) +- Currently all crates share the same version number, in the root Cargo.toml + file. Take note of that version. (If we ever decide to have specific + versions, update those separately as required.) -5. Decide which version to tag the release with (e.g. v0.3.0). Currently we always use the same release number for all crates, but it's possible for them to get out of sync in the future. +- Decide which version to tag the release with (e.g. v0.3.0), considering + [SemVer](https://doc.rust-lang.org/cargo/reference/semver.html). Run `cargo + semver-checks` to check if there are no API changes that break SemVer + compatibility. ([Installation + instructions](https://crates.io/crates/cargo-semver-checks)) Fix issues if + any (i.e. change the version, or revert/adapt the API change). -6. Create new issue. E.g. [Release v0.4.0](https://github.com/ZcashFoundation/frost/issues/377) +- Create new issue. E.g. [Release v0.4.0](https://github.com/ZcashFoundation/frost/issues/377) ## Make changes -7. Bump the version of each crate in their Cargo.toml files +- Bump the version of the crates in the root Cargo.toml file. (If they ever + get out of sync, you will need to bump in each crate Cargo.toml file.) -8. Bump the version used in the tutorial (importing.md) +- Bump the version used in the tutorial (importing.md) -9. Check if the [changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md) is up to date and update if required (we’re only keeping the one in frost-core for now). Double check using [FROST releases](https://github.com/ZcashFoundation/frost/releases) which will have a list of all the PRs that have been closed since the last release. Things to include in the changelog will be anything that impacts production code and big documentation changes. I.e. script and test changes should not be included. NOTE: Please add to the changelog whenever you make changes to the library as this will make things simpler for the person in charge of the release. +- Check if the [changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md) is up to date and update if required (we’re only keeping the one in frost-core for now). Double check using [FROST releases](https://github.com/ZcashFoundation/frost/releases) which will have a list of all the PRs that have been closed since the last release. Things to include in the changelog will be anything that impacts production code and big documentation changes. I.e. script and test changes should not be included. NOTE: Please add to the changelog whenever you make changes to the library as this will make things simpler for the person in charge of the release. - 1. Move version in changelog to Released - 2. Create a new version in “unreleased” in changelog + - Move version in changelog to Released + - Create a new version in “unreleased” in changelog -10. Update the version number for frost-core and frost-rerandomized in the Ciphersuite crates, e.g. in `frost-core = { path = "../frost-core", version = "0.4.0", features = ["test-impl"] }`. You'll need to do this for dependencies and dev-dependencies +- Update the version number for frost-core and frost-rerandomized in the root Cargo.toml file, e.g. in `frost-core = { path = "frost-core", version = "0.4.0", default-features = false }` -11. Create a PR with subject `Release \` containing all these changes +- Create a PR with subject `Release \` containing all these changes -12. You’ll need someone to review and approve it +- You’ll need someone to review and approve it -13. Wait for it to pass CI checks +- Wait for it to pass CI checks ## Publish -14. Checkout main branch, **in the commit of the previously merged PR** (in case other stuff got merged after that) +- Checkout main branch, **in the commit of the previously merged PR** (in case other stuff got merged after that) -15. Run `cargo publish -p frost-core --dry-run` to check if it’s ready to publish. Fix issues if any. +- Run `cargo publish -p frost-core --dry-run` to check if it’s ready to publish. Fix issues if any. -16. [Draft and publish a new release](https://github.com/ZcashFoundation/frost/releases/new) for frost-core. +- [Draft and publish a new release](https://github.com/ZcashFoundation/frost/releases/new) for frost-core. - 1. In “Choose a tag” type `/` e.g. “frost-core/v0.2.0” and click “Create new tag” - 2. In “Target” select “main” as long as other PRs haven’t been merged after the version bump PR. Otherwise, **select the commit matching the PR that was merged above**. - 3. In “Release title” use ` ` e.g. “frost-core v0.2.0” - 4. Paste the (raw Markdown) changelog for this version into the description box. - 5. Leave “Set as pre-release” **unchecked** (we should have checked it in earlier versions but the ship has sailed. It doesn’t matter much) - 6. **Check** “Set as the latest release” + - In “Choose a tag” type `/` e.g. “frost-core/v0.2.0” and click “Create new tag” + - In “Target” select “main” as long as other PRs haven’t been merged after the version bump PR. Otherwise, **select the commit matching the PR that was merged above**. + - In “Release title” use ` ` e.g. “frost-core v0.2.0” + - Paste the (raw Markdown) changelog for this version into the description box. + - Leave “Set as pre-release” **unchecked** (we should have checked it in earlier versions but the ship has sailed. It doesn’t matter much) + - **Check** “Set as the latest release” -17. Publish it with `cargo publish -p frost-core` +- Publish it with `cargo publish -p frost-core` -18. Check if frost-rerandomized is ready to be published: `cargo publish -p frost-rerandomized --dry-run`. Fix any errors if needed. +- Check if frost-rerandomized is ready to be published: `cargo publish -p frost-rerandomized --dry-run`. Fix any errors if needed. -19. Draft and publish a frost-rerandomized release +- Draft and publish a frost-rerandomized release: - 1. Use the same process as described for frost-core above, but you can leave the changelog empty and **uncheck** “Set as the latest release” + - Run `gh release create "frost-rerandomized/v2.1.0" -n '' -t "frost-rerandomized v2.1.0" --latest=false` + (replace both instances of the version) -20. Publish it with `cargo publish -p frost-rerandomized` +- Publish it with `cargo publish -p frost-rerandomized` -21. Check if other crates are ready to be published: `for cs in ristretto255 ed25519 secp256k1 p256 ed448; do cargo publish -p frost-$cs --dry-run; done`. Fix any issues if needed. +- Check if other crates are ready to be published: `for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do cargo publish -p frost-$cs --dry-run; done`. Fix any issues if needed. - 1. If you get an error like this: + - If you get an error like this: “error: failed to verify package tarball Caused by: failed to select a version for the requirement `frost-core = "^0.3.0"` candidate versions found which didn't match: 0.2.0, 0.1.0 location searched: crates.io index required by package `frost-ed25519 v0.3.0 (frost/target/package/frost-ed25519-0.3.0)`” This is because the ciphersuite crates aren’t pointing at the new frost-core package. This is because you need to publish frost-core before you can publish the others otherwise they will not have the expected version to point to. -22. Draft and publish releases for each of those crates (sorry, that will be boring) +- Draft and publish releases for each of those crates: - 1. Use the same process as described for frost-core above (actions 1 - 3), but you can leave the changelog empty and **uncheck** “Set as the latest release” + - Run `for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do gh release create "frost-$cs/v2.1.0" -n '' -t "frost-$cs v2.1.0" --latest=false; done` (replace both instances of the version) -23. Publish those crates: `for cs in ristretto255 ed25519 secp256k1 p256 ed448; do cargo publish -p frost-$cs; done` +- Publish those crates: `for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do cargo publish -p frost-$cs; done` ## Confirm -24. Check versions in the crates to confirm everything worked:  +- Check versions in the crates to confirm everything worked: - 1. [Frost core](https://crates.io/crates/frost-core/versions) - 2. [Frost ed25519](https://crates.io/crates/frost-ed25519/versions) - 3. [Frost ed448](https://crates.io/crates/frost-ed448/versions) - 4. [Frost p256](https://crates.io/crates/frost-p256/versions) - 5. [Frost ristretto255](https://crates.io/crates/frost-ristretto255/versions) - 6. [Frost secp256k1](https://crates.io/crates/frost-secp256k1/versions) - 7. [Frost rerandomized](https://crates.io/crates/frost-rerandomized/versions) + - [Frost core](https://crates.io/crates/frost-core/versions) + - [Frost ed25519](https://crates.io/crates/frost-ed25519/versions) + - [Frost ed448](https://crates.io/crates/frost-ed448/versions) + - [Frost p256](https://crates.io/crates/frost-p256/versions) + - [Frost ristretto255](https://crates.io/crates/frost-ristretto255/versions) + - [Frost secp256k1](https://crates.io/crates/frost-secp256k1/versions) + - [Frost secp256k1 tr](https://crates.io/crates/frost-secp256k1-tr/versions) + - [Frost rerandomized](https://crates.io/crates/frost-rerandomized/versions) -25. Let the team know in the #frost slack channel that the release is complete and successful +- Let the team know in the #frost slack channel that the release is complete and successful ## In the case of an unsuccessful release diff --git a/book/src/frost.md b/book/src/frost.md index dd7591ee0..4b03a3a1f 100644 --- a/book/src/frost.md +++ b/book/src/frost.md @@ -78,11 +78,54 @@ be able to produce the final signature. Of course, the Coordinator is still free to start the process with only 2 participants if they wish. ``` -## Verifying +## Verifying Signatures Signature verification is carried out as normal with single-party signatures, along with the signed message and the group verifying key as inputs. +## Repairing Shares + +Repairing shares allow participants to help another participant recover their +share if they have lost it, or also issue a new share to a new participant +(while keeping the same threshold). + +The repair share functionality requires a threshold of participants to work. +For example, in a 2-of-3 scenario, two participants can help the third recover +their share, or they could issue a new share to move to a 2-of-4 group. + +The functionality works in such a way that each participant running the repair +share function is not able to obtain the share that is being recovered or +issued. + +## Refreshing Shares + +Refreshing shares allow participants (or a subset of them) to update their +shares in a way that maintains the same group public key. Some applications are: + +- Make it harder for attackers to compromise the shares. For example, in a + 2-of-3 threshold scenario, if an attacker steals one participant's device and + all participants refresh their shares, the attacker will need to start over + and steal two shares instead of just one more. +- Remove a participant from the group. For example, in a 2-of-3 threshold + scenario, if two participants decide to remove the third they can both refresh + their shares and the third participant would no longer be able to participate + in signing sessions with the others. (They can also then use the repair share + functionality to issue a new share and move from 2-of-2 back to 2-of-3.) + +```admonish danger +It is critically important to keep in mind that the **Refresh Shares +functionality does not "restore full security" to a group**. While the group +evolves and participants are removed and new participants are added, the +security of the group does not depend only on the threshold of the current +participants being honest, but also **on the threshold of all previous set of +participants being honest**! For example, if Alice, Mallory and Eve form a group +and Mallory is eventually excluded from the group and replaced with Bob, it is +not enough to trust 2 out of 3 between Alice, Bob and Eve. **You also need to +trust that Mallory won't collude with, say, Eve which could have kept her +original pre-refresh share and they could both together recompute the original +key and compromise the group.** If that's an unacceptable risk to your use case, +you will need to migrate to a new group if that makes sense to your application. +``` ## Ciphersuites @@ -90,3 +133,78 @@ FROST is a generic protocol that works with any adequate prime-order group, which in practice are constructed from elliptic curves. The spec specifies five ciphersuites with the Ristretto255, Ed25519, Ed448, P-256 and secp256k1 groups. It's possible (though not recommended) to use your own ciphersuite. + +## Network Topologies + +FROST supports different network topologies for both signing and DKG (Distributed Key Generation) processes. Understanding these topologies is crucial for implementing FROST in a way that best suits your application's needs. + +### Signing Topologies + +#### 1. Centralized Coordinator + +```ascii + Coordinator + / | \ + / | \ + / | \ + Signer1 Signer2 Signer3 +``` + +This is the default topology where: +- A single coordinator (which may or may not be a signer) manages the signing process +- Signers only communicate with the coordinator +- Pros: Simple to implement, clear communication flow +- Cons: Single point of failure, potential bottleneck + +#### 2. Distributed Coordination + +```ascii + Signer1 -------- Signer2 + \ / + \ / + \ / + Signer3 +``` + +In this topology: +- Each signer acts as their own coordinator +- All signers communicate directly with each other +- Pros: No single point of failure +- Cons: More complex implementation, requires full mesh networking + +### DKG Topologies + +#### 1. Full Mesh (Recommended) + +```ascii + Node1 --------- Node2 + | \ / | + | \ / | + | \ / | + | \ / | + | \ / | + Node4 --- Node3 +``` + +For DKG: +- All participants need to communicate directly with each other +- Requires authenticated and confidential channels between all pairs +- Requires a broadcast channel for public values +- Most secure but requires more complex networking setup + +#### 2. Star with Broadcast Hub + +```ascii + Hub + / | \ + / | \ + Node1 | Node3 + | + Node2 +``` + +Alternative DKG setup: +- A central hub relays messages between participants +- Simpler networking requirements +- Hub must be trusted for message delivery (but cannot learn secrets) +- May be suitable for controlled environments \ No newline at end of file diff --git a/book/src/index.md b/book/src/index.md index 66fad0a2c..39ee232b7 100644 --- a/book/src/index.md +++ b/book/src/index.md @@ -2,10 +2,4 @@ This is a guide-level reference for the [ZF FROST library](https://github.com/ZcashFoundation/frost/). -## Getting Started - -If you're not familiar with FROST, first read [Understanding FROST](frost.md). - -Then read the [Tutorial](tutorial.md), and use the [Rust -docs](user.md) as -reference. \ No newline at end of file +{{#include ../../README.md}} \ No newline at end of file diff --git a/book/src/terminology.md b/book/src/terminology.md index 1b9305310..686dfd017 100644 --- a/book/src/terminology.md +++ b/book/src/terminology.md @@ -3,18 +3,40 @@ ### _Broadcast channel_ A secure broadcast channel in the context of multi-party computation protocols -such as FROST has the following properties: - -1. Consistent. Each participant has the same view of the message sent over the channel. -2. Authenticated. Players know that the message was in fact sent by the claimed sender. In practice, this -requirement is often fulfilled by a PKI. -3. Reliable Delivery. Player i knows that the message it sent was in fact received by the intended participants. -4. Unordered. The channel does not guarantee ordering of messages. - -Possible deployment options: -- Echo-broadcast (Goldwasser-Lindell) -- Posting commitments to an authenticated centralized server that is trusted to - provide a single view to all participants (also known as 'public bulletin board') +such as FROST must have a set of theoretical properties which can be a bit subtle +and depend on the specific protocol being implemented. However, most real +deployments use the protocol from the [Secure Computation Without +Agreement](https://eprint.iacr.org/2002/040) paper, which we describe below, and +which is also referred to as "echo broadcast". It has the following properties: +agreement (if an honest party outputs x, then all honest parties output x or +abort), validity (if the broadcaster is honest, then all honest parties output +the broadcast value) and non-triviality (if all parties are honest, they all +output the broadcast value). + +The echo broadcast works as follows, for a party `P[1]` which wants to broadcast +a value `x` to the other `P[i]` parties for `1 < i <= n` where `n` is the number +of participants: + +1. `P[1]` sends `x` to all other `n-1` parties. +2. For each `P[i]` other than `P[1]`: + 1. Set `x1` to the value received from `P[1]` in step 1, or to `null` if no + value was received. + 2. Send `x1` to the other `n-2` parties (excluding `1` and `i` themselves). + 3. Set `r[j]` to the value that `i` will receive from the other `n-2` parties, + indexed by their index `j`. + 4. Output `x1` if it is equal to every value in `r[j]` for all `j` in the + other `n-2` parties. Otherwise, output `null`. + +In the specific context of FROST, you will need to use the echo broadcast for +each participant to send their round 1 package to the other participants. This +means that you will need to run `n` instances of the echo-broadcast protocol +in parallel! + +As an alternative to using echo-broadcast, other mechanisms are possible +depending on the application. For example, posting commitments (round 1 +packages) to an authenticated centralized server. This server needs to be +trusted to provide a single view to all participants (also known as "public +bulletin board"). ### _Identifier_ @@ -32,15 +54,28 @@ This allows deriving identifiers from usernames or emails, for example. ### _Peer to peer channel_ -Peer-to-peer channels are authenticated, reliable, and unordered, per the -definitions above. Additionally, peer-to-peer channels are _confidential_; i.e., -only participants `i` and `j` are allowed to know the contents of -a message `msg_i,j`. +Peer-to-peer channels are required to send data back and forth between +participants (during DKG) and between coordinator and participants (during +signing) in order to use FROST. These channels have different requirements +in different scenarios: + +- They need to be authenticated when sending DKG messages, and when sending + signing messages if cheater detection is required. In this context, + "authenticated" means that the recipient must have assurance on who is the + sender of a message, using e.g. digital signatures. +- They need to be confidential when sending DKG messages, and when sending + signing messages if the messages being signed are confidential. In this + context, "confidential" means that no other party listening to the + communication can have access to the contents, using e.g. encryption. + +In practice there are multiple possible deployment options to achieve +authentication and confidentiality: -Possible deployment options: - Mutually authenticated TLS +- Noise protocol - Wireguard + ### _Threshold secret sharing_ Threshold secret sharing does not require a broadcast channel because the dealer is fully trusted. @@ -51,5 +86,3 @@ Verifiable secret sharing requires a broadcast channel because the dealer is _not_ fully trusted: keygen participants verify the VSS commitment which is transmitted over the broadcast channel before accepting the shares distributed from the dealer, to ensure all participants have the same view of the commitment. - - diff --git a/book/src/tutorial.md b/book/src/tutorial.md index 0f80f4915..15d0a2514 100644 --- a/book/src/tutorial.md +++ b/book/src/tutorial.md @@ -5,8 +5,8 @@ a generic implementation of the protocol, which can't be used directly without a concrete instantiation. The ciphersuite crates (`frost-ristretto255`, `frost-ed25519`, `frost-ed448`, -`frost-p256`, and `frost-secp256k1`) provide ciphersuites to use with -`frost-core`, but also re-expose the `frost-core` functions without +`frost-p256`, `frost-secp256k1` and `frost-secp256k1-tr`) provide ciphersuites +to use with `frost-core`, but also re-expose the `frost-core` functions without generics. If you will only use a single ciphersuite, then we recommend using those functions, and this tutorial will follow this approach. If you need to support multiple ciphersuites then feel free to use @@ -15,3 +15,9 @@ If you need to support multiple ciphersuites then feel free to use This tutorial will use the `frost-ristretto255` crate, but changing to another ciphersuite should be a matter of simply changing the import. +```admonish note +"The `frost-secp256k1` crate is not compatible with Bitcoin BIP-340 (Taproot) +signatures. Use +[frost-secp256k1-tr](https://crates.io/crates/frost-secp256k1-tr) instead +if you want to support it. +``` diff --git a/book/src/tutorial/dkg.md b/book/src/tutorial/dkg.md index ce467b543..91f0a4d0b 100644 --- a/book/src/tutorial/dkg.md +++ b/book/src/tutorial/dkg.md @@ -1,5 +1,9 @@ # Distributed Key Generation +ZF FROST supports a variant of the DKG described in the [original FROST +paper](https://eprint.iacr.org/2020/852.pdf) (the only difference is the absence +of the context string which was deemed unnecessary after further analysis). + The diagram below shows the distributed key generation process. Dashed lines represent data being sent through an [authenticated and confidential communication @@ -47,6 +51,11 @@ the protocol is aborted. Check the linked [Terminology section](https://frost.zfnd.org/terminology.html#broadcast-channel) for more details. +In the context of the DKG, `n` broadcast channels will need to be set up; one +for each participant. So each participant will broadcast their round 1 package +to the other participants, and each participant needs to handle the broadcast +from the other `n-1` participants. + **Failure in using a proper broadcast channel will make the key generation insecure.** ``` @@ -56,9 +65,13 @@ insecure.** Upon receiving the other participants' `round1::Package`s, each participant then calls [`dkg::part2()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/fn.part2.html) -passing their own previously created `round1::SecretPackage` and the list of -received `round1::Packages`. It returns a `round2::SecretPackage` and a -`BTreeMap` mapping other participants's `Identifier`s to `round2::Package`s: +passing their own previously created `round1::SecretPackage` and a map of the +received `round1::Packages`, keyed by the Identifiers of the participant that +sent each one of them. (These identifiers must come from whatever mapping the +coordinator has between communication channels and participants, i.e. they must +have assurance that the `round1::Package` came from the participant with that +identifier.) It returns a `round2::SecretPackage` and a `BTreeMap` mapping other +participants's `Identifier`s to `round2::Package`s: ```rust,no_run,noplayground {{#include ../../../frost-ristretto255/dkg.md:dkg_part2}} @@ -71,15 +84,22 @@ The `round2::Package`s must be sent to their respective participants with the given `Identifier`s, using an [authenticated and confidential communication channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). +```admonish danger +The `round2::Package`s MUST be encrypted, otherwise an attacker who can read +the content of the packages will be able to recreate the secret being +generated. +``` + ## Part 3 Finally, upon receiving the other participant's `round2::Package`, the DKG is concluded by calling [`dkg::part3()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/fn.part3.html) passing the same `round1::Package`s received in Part 2, the `round2::Package`s -just received, and the previously stored `round2::SecretPackage` for the -participant. It returns a `KeyPackage`, with the participant's secret share, -and a `PublicKeyPackage` containing the group verifying key: +just received (again keyed by the Identifier of the participant that sent each +one of them), and the previously stored `round2::SecretPackage` for the +participant. It returns a `KeyPackage`, with the participant's secret share, and +a `PublicKeyPackage` containing the group verifying key: ```rust,no_run,noplayground {{#include ../../../frost-ristretto255/dkg.md:dkg_part3}} diff --git a/book/src/tutorial/importing.md b/book/src/tutorial/importing.md index 7fd3c5631..ca33d6aa6 100644 --- a/book/src/tutorial/importing.md +++ b/book/src/tutorial/importing.md @@ -6,7 +6,7 @@ Add to your `Cargo.toml` file: ``` [dependencies] -frost-ristretto255 = "1.0.0" +frost-ristretto255 = "3.0.0-rc.0" ``` ## Handling errors @@ -25,20 +25,20 @@ help with serialization in the following ways: ### Default byte-oriented serialization With the `serialization` feature, which is enabled by default, all structs that -need to communicated will have `serialize()` and `deserialize()` methods. The +need to be communicated will have `serialize()` and `deserialize()` methods. The serialization format is described in [Serialization Format](../user/serialization.md). ### serde -Alternatively, if you would like to user another format such as JSON, you can +Alternatively, if you would like to use another format such as JSON, you can enable the `serde` feature (which is *not* enabled by default). When it is enabled, you can use [serde](https://serde.rs/) to serialize any structure that needs to be transmitted. The importing would look like: ``` [dependencies] -frost-ristretto255 = { version = "0.7.0", features = ["serde"] } +frost-ristretto255 = { version = "3.0.0-rc.0", features = ["serde"] } ``` Note that serde usage is optional. Applications can use different encodings, and diff --git a/book/src/tutorial/refreshing-shares.md b/book/src/tutorial/refreshing-shares.md new file mode 100644 index 000000000..fa45ce914 --- /dev/null +++ b/book/src/tutorial/refreshing-shares.md @@ -0,0 +1,40 @@ +# Refreshing Shares using a Trusted Dealer + +The diagram below shows the refresh share process. Dashed lines +represent data being sent through an [authenticated and confidential communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + +![Diagram of Refreshing shares, illustrating what is explained in the text](refreshing.png) + +The Trusted Dealer needs to first run `compute_refreshing_shares()` which +returns SecretShares (the "refreshing shares") and a PublicKeyPackage. Each +`SecretShare` must then be sent along with the `PublicKeyPackage` via an +[**authenticated** and **confidential** channel +](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) for each +participant. + +Each Participant then runs `refresh_share()` to generate a new `KeyPackage` +which will replace their old `KeyPackage`; they must also replace their old +`PublicKeyPackage` with the one sent by the Trusted Dealer. + +```admonish danger +The refreshed `KeyPackage` contents must be stored securely and the original +`KeyPackage` should be deleted. For example: + +- Make sure other users in the system can't read it; +- If possible, use the OS secure storage such that the package + contents can only be opened with the user's password or biometrics. +``` + +```admonish danger +Applications should first ensure that all participants who refreshed their +`KeyPackages` were actually able to do so successfully, before deleting their old +`KeyPackages`. How this is done is up to the application; it might require +successfully generating a signature with all of those participants. +``` + +```admonish danger +Refreshing Shares may be not enough to address security concerns +after a share has been compromised. Refer to the [Understanding +FROST](../frost.md#refreshing-shares) section. +``` \ No newline at end of file diff --git a/book/src/tutorial/refreshing.png b/book/src/tutorial/refreshing.png new file mode 100644 index 000000000..34aa79456 Binary files /dev/null and b/book/src/tutorial/refreshing.png differ diff --git a/book/src/tutorial/signing.md b/book/src/tutorial/signing.md index e978fa42b..18b120c46 100644 --- a/book/src/tutorial/signing.md +++ b/book/src/tutorial/signing.md @@ -9,7 +9,7 @@ channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). ## Coordinator, Round 1 To sign, the -[Coordinator](file:///home/conrado/zfnd/frost/book/book/frost.html#signing) must +[Coordinator](../frost.md#signing) must select which participants are going to generate the signature, and must signal to start the process. This needs to be implemented by users of the ZF FROST library and will depend on the communication channel being used. @@ -25,8 +25,13 @@ their commitments (a `SigningCommitments`) by calling ``` The `SigningNonces` must be kept by the participant to use in Round 2, while the -`SigningCommitments` must be sent to the Coordinator using an [authenticated -channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). +`SigningCommitments` must be sent to the Coordinator. + +```admonish info +FROST does not require using an [authenticated nor encrypted +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) +during the **signing** process. +``` ## Coordinator, Round 2 @@ -38,10 +43,9 @@ message to be signed, and then build a `SigningPackage` by calling {{#include ../../../frost-ristretto255/README.md:round2_package}} ``` -The `SigningPackage` must then be sent to all the participants using an -[authenticated -channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). (Of course, -if the message is confidential, then the channel must also be confidential.) +The `SigningPackage` must then be sent to all the participants. (If the message +is confidential, then the channel must also be confidential, since the message +is included in the `SigningPackage`.) ```admonish warning In all of the main FROST ciphersuites, the entire message must @@ -63,9 +67,7 @@ their `SigningNonces` from Round 1, by calling {{#include ../../../frost-ristretto255/README.md:round2_sign}} ``` -The resulting `SignatureShare` must then be sent back to the Coordinator using -an [authenticated -channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). +The resulting `SignatureShare` must then be sent back to the Coordinator. ```admonish important In most applications, it is important that the participant must be aware of what @@ -94,12 +96,26 @@ in the `SigningPackage` in Round 2 for the group verifying key in the `PublicKey FROST supports identifiable abort: if a participant misbehaves and produces an invalid signature share, then aggregation will fail and the returned error will have the identifier of the misbehaving participant. (If multiple participants -misbehave, only the first one detected will be returned.) +misbehave, only the first one detected will be returned. If you need to detect +all cheaters, use [`aggregate_custom()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/fn.aggregate_custom.html)) What should be done in that case is up to the application. The misbehaving participant could be excluded from future signing sessions, for example. ``` +```admonish danger +In `aggregate()` you need to provide a map from `Identifier` to +`SignatureShare`. If you need cheater detection, then it is important that these +identifiers come from a mapping between authenticated channels and identifiers; +i.e. you should not simply send the `Identifier` along with the +`SignatureShare`; otherwise the cheater could simply lie about their identifier. + +For example, if you authenticate the communication channels with TLS, then you +will need to create a public key -> identifier mapping, and use that mapping +to get the identifier for the connection where the `SignatureShare` was read +from. +``` + ## Verifying signatures diff --git a/book/src/tutorial/tkg.png b/book/src/tutorial/tkg.png index 4b9fa2bd0..1ae5cc39e 100644 Binary files a/book/src/tutorial/tkg.png and b/book/src/tutorial/tkg.png differ diff --git a/book/src/tutorial/trusted-dealer.md b/book/src/tutorial/trusted-dealer.md index 999485d53..ee0700855 100644 --- a/book/src/tutorial/trusted-dealer.md +++ b/book/src/tutorial/trusted-dealer.md @@ -40,8 +40,8 @@ You can specify which identifiers to use by using [`IdentifierList::Custom`](htt Which [**authenticated** and **confidential** channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) to use is up to the application. Some examples: -- Manually require the dealer to sent the `SecretShare`s to the - partipants using some secure messenger such as Signal; +- Manually require the dealer to send the `SecretShare`s to the + participants using some secure messenger such as Signal; - Use a TLS connection, authenticating the server with a certificate and the client with some user/password or another suitable authentication mechanism; @@ -59,7 +59,7 @@ to generate signatures. ``` ```admonish danger -The `SecretPackage` contents must be stored securely. For example: +The `KeyPackage` contents must be stored securely. For example: - Make sure other users in the system can't read it; - If possible, use the OS secure storage such that the package diff --git a/book/src/user.md b/book/src/user.md index 30c1cc8b5..f0c4635e9 100644 --- a/book/src/user.md +++ b/book/src/user.md @@ -7,3 +7,4 @@ - [frost-p256](https://docs.rs/frost-p256/) - [frost-ristretto255](https://docs.rs/frost-ristretto255/) - [frost-secp256k1](https://docs.rs/frost-secp256k1/) +- [frost-secp256k1-tr](https://docs.rs/frost-secp256k1-tr/) diff --git a/book/src/user/frost-secp256k1-tr.md b/book/src/user/frost-secp256k1-tr.md new file mode 100644 index 000000000..733406447 --- /dev/null +++ b/book/src/user/frost-secp256k1-tr.md @@ -0,0 +1 @@ +{{#include ../../../frost-secp256k1-tr/README.md}} \ No newline at end of file diff --git a/book/src/user/frost-secp256k1-tr/dkg.md b/book/src/user/frost-secp256k1-tr/dkg.md new file mode 100644 index 000000000..b93feef5f --- /dev/null +++ b/book/src/user/frost-secp256k1-tr/dkg.md @@ -0,0 +1 @@ +{{#include ../../../../frost-secp256k1-tr/dkg.md}} \ No newline at end of file diff --git a/book/src/user/zeroization.md b/book/src/user/zeroization.md new file mode 100644 index 000000000..a8647fdb3 --- /dev/null +++ b/book/src/user/zeroization.md @@ -0,0 +1,15 @@ +# Zeroization + +The ZF FROST crates have limited best-effort support at zeroization. The +top-level structs (`KeyPackage`, `SecretShare`, etc.) implement the +`Zeroize` and `ZeroizeOnDrop` from the `zeroize` crate. This means that +when they are dropped they are cleared from memory. + +However, be advised that the user is responsible for everything else. For +example, if you serialize the structs, then you will be responsible for +zeroizing the serialized buffers, which _will_ contain secrets. + +Additionally, if you extract the secret fields (e.g. `KeyPackage::signing_share()`) +they you are also responsible for zeroizing them if you make a copy, since +the inner types do not implement `ZeroizeOnDrop` (though most of them do +implement `Zeroize` so you can call `zeroize()` manually). diff --git a/book/src/zcash/devtool-demo.md b/book/src/zcash/devtool-demo.md new file mode 100644 index 000000000..e8e713ae9 --- /dev/null +++ b/book/src/zcash/devtool-demo.md @@ -0,0 +1,333 @@ +# zcash-devtool Tutorial + +This tutorial explaining how to use FROST to sign a Zcash transaction using +[zcash-devtool](https://github.com/zcash/zcash-devtool). + + +## Setting up + +Install `cargo` and `git`. + +Install the `zcash-devtool`: + +``` +cargo install --git https://github.com/zcash/zcash-devtool.git --locked +``` + +Install the `frost-client` tool: + +``` +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frost-client +``` + +Install the `zcash-sign` tool: + +``` +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked zcash-sign +``` + +Switch to an empty folder which will store the files generated in the demo. +For example: + +``` +mkdir frost-demo +cd frost-demo/ +``` + + +### Running the server + +This demo uses the ZF FROST server (frostd) to help participants communicate. +While in practice users would use an existing online server, for the demo you +can run a local server by following [these instructions](./server.md) (the +"Compiling, Running and Deploying" and "Local Testing" sections). + +The rest of the tutorial assumes the server is up and running. + + +### Initializing the users + +Run the following command to initialize three users (in practice, each user +would run a similar command, but for demo purposes we're assuming +you will simulate all of them in the same machine, so run these +commands in your machine): + +``` +frost-client init -c alice.toml +frost-client init -c bob.toml +frost-client init -c eve.toml +``` + +This will create a config file for three users; Alice, Bob and Eve. + +```admonish note +If you really want to run the demo in separate machines, then you can omit the +`-c alice.toml` part of the command (i.e. run `frost-client init`); it will +save to a default location in the user's home directory. +``` + + +## Generating FROST key shares + +First we will generate the FROST key shares. For simplicity we'll use trusted +dealer; if you want to use Distributed Key Generation, skip to the next section. + +In a new terminal (in case the previous terminal is running the server), run the +following: + +``` +frost-client trusted-dealer -d "Alice, Bob and Eve's group" --names Alice,Bob,Eve -c alice.toml -c bob.toml -c eve.toml -C redpallas +``` + +This will by default generate a 2-of-3 key shares. The key shares will be +written into each participant's config file. You can change the threhsold, +number of shares and file names using the command line; append `-h` to the +commend above for the command line help. + + +## Generating FROST key shares using DKG + +For real-word usage we commend generating key shares using Distributed Key +Generation. If you did the previous section, skip to "Generating the Full +Viewing Key for the wallet". + + +```admonish note +This section assumes each participant is running the commands in their own +machine. If you want to simulate all of them in a single machine, +specify the config file for the user (e.g. `-c alice.toml`) accordingly. +``` + + +### Initializing config files + +If they haven't yet, each participant should run: + +``` +frost-client init +``` + + +### Sharing contacts + +Each participant must now generate a contact string that they will need to share +with the other participants. This contact string will include a name, which they +can choose when exporting and will be shown to whoever they send the contact to. + +Run the following, substituting the name accordingly: + +``` +frost-client export --name 'Alice' +``` + +The command will print an encoded contact string such as +`zffrost1qyqq2stvd93k2g84hudcr98zp67a9rnx9v00euw9e5424hjathvre7ymy344fynjdvxmwxfg`. +Send it to the other participants using some trusted communication channel +(instant messaging, etc.). + +The other participants will send you their contacts. Import them by running the +following command for each contact (replace `` with the contact +string accordingly): + +``` +frost-client import +``` + + +### Generating shares + +Finally, to generate the shares, one of the participants will need to initiate +the process. They will need to public key of each participant, so they need to +first list them with the following command: + +``` +frost-client contacts +``` + +Then run the following command, replacing the `` and `` hex +strings with the public keys of the contacts which will participate (along with +the user running the command): + +``` +frost-client dkg -d "Alice, Bob and Eve's group" -s localhost:2744 -S , -t 2 -C redpallas -c alice.toml +``` + +The user should then notify the others that a signing session has started (e.g. +via instant messaging again), and also share the threshold number that was used. +They should then run the following, replacing the name of the group if they wish +and the threshold number with the one given by the first participant. + +``` +frost-client dkg -d "Alice, Bob and Eve's group" -s localhost:2744 -t 2 -C redpallas +``` + +```admonish note +A future version might not require specifying the threshold and group name. +``` + + +## Generating the Full Viewing Key for the wallet + +Next, we will need to generate a Zcash Full Viewing Key from the FROST group +material we have just generated; this address will then be imported into a wallet +so that we'll be able to create Zcash transactions for it. + +Run the following command: + +``` +frost-client groups +``` + +It will list all groups you're in - at this point it should list the only one +you have just created. Copy the Public Key it shows (it will look like e.g. +`79d6bcee79c88ad9ba259067772b97f5de12f1435b474d03bc98f255be08a610`) + +The run the following command, replacing `` with the value you copied, +and `test` with `main` if you're using Mainnet. + +``` +zcash-sign generate --net test --ak +``` + +It will print an Orchard address, and a Unified Full Viewing Key. Copy and +paste both somewhere to use them later. + + +## Importing the Full Viewing Key into zcash-devtool + +In the zcash-devtool folder, run the following, replacing `` with the +UFVK printed in the last step: + +``` +zcash-devtool wallet -w ./.frost.view/ init-fvk --name FROST_wallet --fvk --birthday 3720000 -s zecrocks +``` + +(Change `./.frost-view` or `FROST_wallet` if you want to change the folder or +name of the wallet.) + + +## Funding the wallet + +Now you will need to fund this wallet with some ZEC. Send ZEC to that address +using another account (or try [ZecFaucet](https://zecfaucet.com/)). + +## Creating the transaction + +Run the following, replacing `` with the address you want to ZEC to, +`` with the value you want to send in Zatoshis, `` with the +memo you want to send: + +``` +zcash-devtool pczt -w ./.frost.view/ create --address --value --memo > frost_pczt.created +``` + + +## Signing the transaction + +Now you will need to simulate two participants and a Coordinator to sign the +transaction, and you should still have the FROST server running which will +handle communications between them. It's probably easier to open three new +terminals. + +Go back to the signer terminal and run the following, replacing `` +with the path to the file you saved in the previous step, and `` +with the path where you want to write the signed transaction (e.g. +`frost_pczt.signed`). + +``` +zcash-sign sign -n test --tx-plan frost_pczt.created -o frost_pczt.signed +``` + +(Replace `test` with `main` if you're using Mainnet.) + +The program will print a SIGHASH and a Randomizer, and will prompt for a +signature. This is what you will get after running FROST, so let's do that; +leave the prompt there without typing anything. + + +### Coordinator + +In the second terminal, the Coordinator, run (in the same folder where you +initialized the users and ran the key generation) the following: + +``` +frost-client groups -c alice.toml +``` + +This will list the groups Alice is in; it should only list the one you created +earlier. You will need to copy some values in the command. Run the following, +replacing the value after `` with the "Public key" listed for the group; +replacing `` and `` with the public keys of Alice and Bob (the +hexadecimal values printed next to their names; Alice's name will be empty to +indicate it's her own). + +``` +frost-client coordinator -c alice.toml --server-url localhost:2744 --group -S , -m - -r - +``` + +It will prompt you for a message. Paste the SIGHASH generated with the +`zcash-sign` tool and press enter. It will then prompt for a randomizer. Paste +the one generated with the `zcash-sign` tool and press enter. + +The tool will connect to the server and wait for the other participants. + +```admonish warning +If you prefer to pass the message (SIGHASH) or randomizer as files by using +the `-m` and `-r` arguments, you will need to convert them to binary format. +``` + + +### Participant 1 (Alice) + +In the third terminal, Participant 1, run the following (replacing `` +with the same group public key used in the previous command): + +``` +frost-client participant -c alice.toml --server-url localhost:2744 --group +``` + +(We are using "Alice" again. There's nothing stopping a Coordinator from being a +Partcipant too!) + + +### Participant 2 (Bob) + +In the fourth terminal, for Participant 2, run the following (replacing `` +again): + +``` +frost-client participant -c bob.toml --server-url localhost:2744 --group +``` + + +### Coordinator + +Go back to the Coordinator CLI. The protocol should run and complete +successfully. It will print the final FROST-generated signature. Hurrah! Copy it +(just the hex value). + +Go back to the signer and paste the signature. It will write the signed +transaction to the file you specified. + +## Proving the transaction + +You will need to prove the transaction separately: + +``` +cargo run -p zcash-sign -- sign -n test --tx-plan frost_pczt.created -o frost_pczt.signed +``` + +And then combine signed and proven into a final transaction: + +``` +zcash-devtool pczt combine -i frost_pczt.signed -i frost_pczt.proven > frost_pczt.combined +``` + + +## Broadcasting the transaction + +Run: + +``` +zcash-devtool pczt -w ./.frost.view/ send -s zecrocks < test_pczt.combined +``` diff --git a/book/src/zcash/server.md b/book/src/zcash/server.md new file mode 100644 index 000000000..63503946b --- /dev/null +++ b/book/src/zcash/server.md @@ -0,0 +1,363 @@ +# ZF FROST Server (frostd) + +One challenge for using FROST is allowing participants to communicate securely +with one another. Devices are usually behind firewalls and NATs, which make +direct connections hard. + +To mitigate this issue and to make it easier to use FROST, the ZF FROST Server +(frostd) was created. It is a JSON-HTTP server with a small API to allow +participants to create signing sessions and to communicate with one another. + +It works like this: + +- Clients (coordinator or participants) authenticate to the server using a key + pair, which will likely be the same key pair they use to end-to-end encrypt + messages. +- The Coordinator creates a session, specifying the public keys of the + participants. +- Participants list sessions they're participating in, and choose the proceed + with the signing session. +- Coordinator and Participants run the FROST protocol, end-to-end encrypting + messages and sending them to the server. +- The Coordinator closes the session. + +Note that the server doesn't really care about the particular key pair being +used; it is only used to enforce who can send messages to who. + +## Compiling, Running and Deploying + +You will need to have [Rust and +Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) +installed. Run: + +``` +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frostd +``` + +The `frostd` binary will be installed [per `cargo` +config](https://doc.rust-lang.org/cargo/commands/cargo-install.html#description) +and it will likely be in your `$PATH`, so you can run by simply running +`frostd`. + +To deploy the FROST Server, **you need TLS/HTTPS certificates**. We strongly +recommend using a reverse proxy such as `nginx` to handle TLS and to also add +denial of service protections. In that case, use the `--no-tls-very-insecure` +flag in `frostd` and make `nginx` connect to it (see example config below). + +If you want to expose `frostd` directly, use the `--tls-cert` and +`--tls-key` to specify the paths of the PEM-encoded certificate and key. You can +use [Let's Encrypt](https://letsencrypt.org/) to get a free certificate. + + +### Local Testing + +For local testing, you can use the [`mkcert` +tool](https://github.com/FiloSottile/mkcert). Install it and run: + +``` +mkcert -install +mkcert localhost 127.0.0.1 ::1 +``` + +Then start the server with: + +``` +frostd --tls-cert localhost+2.pem --tls-key localhost+2-key.pem +``` + + +### Sample nginx Config + +This is a sample nginx config file tested in a Ubuntu deployment (i.e. it +assumes it's in a `http` block and it's included by `/etc/nginx/nginx.conf`); +copy it to `/etc/nginx/sites-enabled/frostd` and run `sudo service nginx +restart`. + +The config assumes the certificates were copied to `/etc/ssl`. + + +``` +limit_req_zone $binary_remote_addr zone=challenge:10m rate=30r/m; +limit_req_zone $binary_remote_addr zone=create:10m rate=10r/m; +limit_req_zone $binary_remote_addr zone=other:10m rate=240r/m; +limit_conn_zone $binary_remote_addr zone=addr:10m; + +server { + listen 443 ssl; + listen [::]:443 ssl; + ssl_certificate /etc/ssl/localhost+2.pem; + ssl_certificate_key /etc/ssl/localhost+2-key.pem; + ssl_protocols TLSv1.3; + ssl_ecdh_curve X25519:prime256v1:secp384r1; + ssl_prefer_server_ciphers off; + + server_name localhost; + + client_body_timeout 5s; + client_header_timeout 5s; + + location / { + proxy_pass http://127.0.0.1:2744; + limit_req zone=other burst=5; + limit_conn addr 10; + } + location /challenge { + proxy_pass http://127.0.0.1:2744/challenge; + limit_req zone=challenge burst=3; + limit_conn addr 10; + } + location /create_new_session { + proxy_pass http://127.0.0.1:2744/create_new_session; + limit_req zone=create burst=3; + limit_conn addr 10; + } +} +``` + +## API + +The API uses JSON/HTTP. All requests should have `Content-Type: +application/json`. Errors are returned with status code 500 and the content +body will have a JSON such as: + +``` +{ code: 1, msg: "error message" } +``` + +The +[codes](https://github.com/ZcashFoundation/frost-zcash-demo/blob/548a8a7329c6eed8180464662f430d12cd71dfcc/frostd/src/lib.rs#L95-L98) +are: + +``` +pub const INVALID_ARGUMENT: usize = 1; +pub const UNAUTHORIZED: usize = 2; +pub const SESSION_NOT_FOUND: usize = 3; +pub const NOT_COORDINATOR: usize = 4; +``` + + +### Usage flow + +For the Coordinator: + +- Log in with `/challenge` and `/login` +- Create a new signing session with `/create_new_session` +- Wait for round 1 messages by repeatedly polling `/receive` each 2 seconds or longer +- Send round 2 messages by using `/send` +- Wait for round 2 message by repeatedly polling `/receive` each 2 seconds or longer +- Close the session with `/close_session` + +For Participants: + +- Log in with `/challenge` and `/login` +- Wait for signing sessions with `/list_sessions`, either by the user's request or by repeatedly + polling each 10 seconds or longer +- Get the session information with `/get_session_info` +- Show the user the session information (who the participants are) to select which + session (if more than one) +- Send round 1 message by using `/send` +- Wait for round 2 message by repeatedly polling `/receive` each 2 seconds or longer +- Send round 2 message by using `/send` + +```admonish info +**Polling** is not optimal. The server will support a better mechanism in the +future. +``` + +```admonish info +Selecting sessions is tricky. Ideally, the user should select what session +to proceed by checking the message being signed; however, that is usually +sent in Round 2. There are multiple ways to handle this: + +- Simply show the users who are participants, hoping that is enough to + disambiguate (we assume that concurrent signing sessions won't be that common) +- Quietly proceed with all sessions, and only prompt the user after the message + is received. (It's harmless to do round 1 of FROST even if the user might + not have agreed to sign the message yet.) +- Change the application so that the message is sent to the participants first + (the server does not really care how the protocol is run). +``` + +```admonish critical +Always gather consent from the user by showing them the message before +signing it. +``` + +### `/challenge` + +Input: empty + +Sample output: + +``` +{"challenge":"2c5cdb6d-a7db-470e-9e6f-2a7062532825"} +``` + +Returns a challenge that the client will need to sign in order to authenticate. + +### `/login` + +To call `/login`, you will need to sign the challenge with XEdDSA, see +[example](https://github.com/ZcashFoundation/frost-zcash-demo/blob/548a8a7329c6eed8180464662f430d12cd71dfcc/frostd/tests/integration_tests.rs#L443-L476). +Sign the challenge UUID, converted to bytes. + + +Input sample: + +``` +{ + "challenge":"b771757e-085a-4a88-ab8f-28bd4ba67f3a", + "pubkey":"f5bf1b8194e20ebdd28e662b1efcf1c5cd2aaade5d5dd83cf89b246b5492726b", + "signature":"bba398d0963ab88e28134ad41c127eeee816a219838db01dd7bcd9d7fcd975f082330c134e6f7238580ba8434652aa116891495452d9048f5615e07f4ad6b204" +} +``` + +Output sample: + +``` +{"access_token":"061a18ba-2c3c-4685-a79e-2c0c93000af5"} +``` + +The returned access token must be included as a bearer token in an +`Authorization` header; e.g. `Authorization: Bearer +061a18ba-2c3c-4685-a79e-2c0c93000af5`. + +Access tokens are currently valid for 1 hour. It's recommended to login at the +beginning of each FROST session; log in again if it needs to take longer. + +### `/logout` + +Input: empty (it will logout the authenticated user) + +Output: empty + +Logs out, invalidating the access token. Note that access tokens expire after +1 hour anyway. + +### `/create_new_session` + +Input sample: + +``` +{ + "pubkeys": [ + "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", + "edbd661dec0a9d0468b4a166a4afa80560d769f6bcb152fb8f4224059329a518" + ], + message_count: 1, +} +``` + +Output sample: + +``` +{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"} +``` + +Creates a new session. The requesting user will be the Coordinator, and the +users with the hex-encoded public keys given in `pubkeys` will be the +participants (which might or might not include the Coordinator itself). + +The `message_count` parameter allows signing more than one message in the same +signing session, which will save roundtrips. This does not impacts the server +itself and is used to signal the participants (via `/get_session_info`). + +### `/list_sessions` + +Input: empty (it will list for the authenticated user) + +Output sample: + +``` +{"session_ids": ["2c5cdb6d-a7db-470e-9e6f-2a7062532825"]} +``` + +List the sessions IDs of the session a participant is in. + +### `/get_session_info` + +Input sample: + +```{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"}``` + +Output sample: + +``` +{ + "message_count": 1, + "pubkeys": [ + "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", + "edbd661dec0a9d0468b4a166a4afa80560d769f6bcb152fb8f4224059329a518" + ], + "coordinator_pubkey": "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", +} +``` + +Returns information about the given session. + +### `/send` + +Input sample: + +``` +{ + "session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825", + "recipients": ["3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c"], + "msg": "000102", +} +``` + +Output: empty + +Sends a (hex-encoded) message to one or more participants. To send to the +Coordinator, pass an empty list in `recipients` (**do not** use the +Coordinator's public key, because that might be ambiguous if they're also a +Participant). + +```admonish critical +Messages **MUST** be end-to-end encrypted between recipients. The server can't +enforce this and if you fail to encrypt them then the server could read +all the messages. +``` + +### `/receive` + +Input sample: + +``` +{ + "session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825", + "as_coordinator": false, +} +``` + +Output sample: + +``` +{ + "msgs":[ + { + "sender": "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", + "msg": "000102", + } + ] +} +``` + +Receives messages sent to the requesting user. Note that if a user is both a +Coordinator and a Participant, it is not possible to distinguish if a message +received from them was sent as Coordinator or as a Participant. This does not +matter in FROST since this ambiguity never arises (Participants always receive +messages from the Coordinator, and vice-versa, except during DKG where there is +no Coordinator anyway). + +### `/close_session` + +Input sample: + +```{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"}``` + +Output: empty + +Closes the given session. Only the Coordinator who created the session can close +it. Sessions also expire by default after 24 hours. diff --git a/book/src/zcash/technical-details.md b/book/src/zcash/technical-details.md index 76cd1207c..e75adfe7d 100644 --- a/book/src/zcash/technical-details.md +++ b/book/src/zcash/technical-details.md @@ -1,7 +1,7 @@ # Technical Details -FROST only works with Schnorr signatures. Zcash transaprent transactions use -ECDSA, therefore FROST does not work with Zcash transaparent addresses. (This +FROST only works with Schnorr signatures. Zcash transparent transactions use +ECDSA, therefore FROST does not work with Zcash transparent addresses. (This could change if the Taproot upgrade from Bitcoin is ported to Zcash, but it seems unlikely.) @@ -18,7 +18,7 @@ protocol](https://zips.z.cash/protocol/protocol.pdf#addressesandkeys): To use FROST with Zcash, the key that needs to be split is the **Spend Authorizing Key** or `ask`. This is the key that signs transactions and allow -they to go through. +them to go through. ## Key Derivation and DKG Support @@ -70,7 +70,7 @@ For this reason it seems impossible to easily encode a FROST wallet, so using something like a JSON file with all this information is advisable. Of course, unlike regular Zcash wallets, a user losing their FROST wallet is -not catastrophical. Users can recover their key share with the help of other +not catastrophic. Users can recover their key share with the help of other participants, and would only need to remember their identifier (and other participants can probably help with that). @@ -90,14 +90,14 @@ when recovering a wallet. The biggest challenge in using FROST with Zcash is allowing participants to communicate securely with each other, which is required to run the FROST -protocol. Since wallets don't currently need to communication to each other, a +protocol. Since wallets don't currently need to communicate with each other, a whole new mechanism will need to be implemented. For this to happen, two things are required: - Allowing wallets to actually communicate with each other (regardless of security). This is challenging because users are usually behind NATs and - firewalls, so they can't simply open TCP connections with each other. So + firewalls, so they can't simply open TCP connections with each other. So, some kind of signaling server may be needed. - Making the communication secure. This is actually fairly solvable while not trivial and we're planning on working on a library to address it. It needs to diff --git a/book/src/zcash/ywallet-demo.md b/book/src/zcash/ywallet-demo.md index cf3491556..1972695ac 100644 --- a/book/src/zcash/ywallet-demo.md +++ b/book/src/zcash/ywallet-demo.md @@ -1,235 +1,329 @@ # Ywallet Demo Tutorial -This tutorial explaing how to run the FROST demo using Ywallet that was -[presented during Zcon4](https://www.youtube.com/watch?v=xvzESdDtczo). - -## Information - -1. The Trusted Dealer journey -2. RedPallas -3. YWallet -4. Sprout -5. [Sapling](https://docs.rs/reddsa/0.5.1/reddsa/sapling/index.html) -6. [frost-ed25519 crate](https://crates.io/crates/frost-ed25519) +This tutorial explaining how to run the FROST demo using Ywallet that was +[presented during Zcon4](https://www.youtube.com/watch?v=xvzESdDtczo) (though it +has been updated and it differs from what was presented). Ywallet supports [offline signing](https://ywallet.app/advanced/offline_signature/), which allows having a view-only account that can generate a transaction plan, which can be signed by -a offline wallet also running Ywallet. The demo uses this mechanism but signs +an offline wallet also running Ywallet. The demo uses this mechanism but signs the transaction plan with a command line tool, using FROST. This tutorial assumes familiarity with the command line. + ## Setting up Install `cargo` and `git`. [Install Ywallet](https://ywallet.app/installation/). -Clone the repositories: +Install the `frost-client` tool: + +``` +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frost-client +``` + +Install the `zcash-sign` tool: + +``` +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked zcash-sign +``` + +Switch to an empty folder which will store the files generated in the demo. +For example: ``` -git clone https://github.com/ZcashFoundation/frost-zcash-demo.git -git clone --recurse-submodules --branch frost-demo https://github.com/ZcashFoundation/zwallet.git -git clone https://github.com/ZcashFoundation/zcash.git +mkdir frost-demo +cd frost-demo/ ``` -Download Sprout and Sapling parameters: +### Running the server + +This demo uses the ZF FROST server (frostd) to help participants communicate. +While in practice users would use an existing online server, for the demo you +can run a local server by following [these instructions](./server.md) (the +"Compiling, Running and Deploying" and "Local Testing" sections). -[Sprout params](https://download.z.cash/downloads/sprout-groth16.params) +The rest of the tutorial assumes the server is up and running. -[Sapling spend params](https://download.z.cash/downloads/sapling-spend.params) -[Sapling output params](https://download.z.cash/downloads/sapling-output.params) +### Initializing the users -Move the params files into `zwallet/native/zcash-params/src/` +Run the following command to initialize three users (in practice, each user +would run a similar command, but for demo purposes we're assuming +you will simulate all of them in the same machine, so run these +commands in your machine): + +``` +frost-client init -c alice.toml +frost-client init -c bob.toml +frost-client init -c eve.toml +``` + +This will create a config file for three users; Alice, Bob and Eve. + +```admonish note +If you really want to run the demo in separate machines, then you can omit the +`-c alice.toml` part of the command (i.e. run `frost-client init`); it will +save to a default location in the user's home directory. +``` ## Generating FROST key shares First we will generate the FROST key shares. For simplicity we'll use trusted -dealer, DKG will be described later. +dealer; if you want to use Distributed Key Generation, skip to the next section. -Run the following (it will take a bit to compile): +In a new terminal (in case the previous terminal is running the server), run the +following: ``` -cd frost-zcash-demo/ -cargo run --bin trusted-dealer --features redpallas +frost-client trusted-dealer -d "Alice, Bob and Eve's group" --names Alice,Bob,Eve -c alice.toml -c bob.toml -c eve.toml -C redpallas ``` -Answer the prompts with `2` (minimum number of signers), `3` -(maximum) and empty, pressing enter to submit each. +This will by default generate a 2-of-3 key shares. The key shares will be +written into each participant's config file. You can change the threhsold, +number of shares and file names using the command line; append `-h` to the +commend above for the command line help. -A bunch of information will be printed. Copy and paste them somewhere to use -them later, or leave the terminal open. -```admonish info -If you want to use DKG instead of Trusted Dealer, instead of the command above, - run this for each participant, in separate terminals for each: +## Generating FROST key shares using DKG -`cargo run --bin dkg --features redpallas` +For real-word usage we commend generating key shares using Distributed Key +Generation. If you did the previous section, skip to "Generating the Full +Viewing Key for the wallet". -and follow the instructions. (There will be a considerable amount of -copy&pasting!) + +```admonish note +This section assumes each participant is running the commands in their own +machine. If you want to simulate all of them in a single machine, +specify the config file for the user (e.g. `-c alice.toml`) accordingly. ``` -## Generating the Full Viewing Key for the wallet -In a new terminal, switch to the folder of the signer tool: +### Initializing config files + +If they haven't yet, each participant should run: + +``` +frost-client init +``` + + +### Sharing contacts + +Each participant must now generate a contact string that they will need to share +with the other participants. This contact string will include a name, which they +can choose when exporting and will be shown to whoever they send the contact to. +Run the following, substituting the name accordingly: ``` -cd zwallet/native/zcash-sync/ +frost-client export --name 'Alice' ``` -Before running it, you will need to create a seed phrase which is used to -generate the Sapling address. This wouldn't be needed since the demo only works -with an Orchard address, but due to current limitations in the underlying -crates, we also need to generate a Sapling address which won't be used in the -demo. Generate a fresh 24-word seed phrase, for example using [this -site](https://iancoleman.io/bip39/) (reminder: don't use random sites to -generate seed phrases unless for testing purposes!), then write to a file called -`.env` in the signer folder in the following format, putting the seed phrase -inside the quotes: +The command will print an encoded contact string such as +`zffrost1qyqq2stvd93k2g84hudcr98zp67a9rnx9v00euw9e5424hjathvre7ymy344fynjdvxmwxfg`. +Send it to the other participants using some trusted communication channel +(instant messaging, etc.). - ``` - KEY="seed phrase" - ``` +The other participants will send you their contacts. Import them by running the +following command for each contact (replace `` with the contact +string accordingly): -We can finally generate a new wallet. Run the following command; it will -take a bit to compile. It will show a bunch of warnings which is normal. +``` +frost-client import +``` + + +### Generating shares + +Finally, to generate the shares, one of the participants will need to initiate +the process. They will need to public key of each participant, so they need to +first list them with the following command: ``` -cargo run --release --bin sign --features dotenv -- -g +frost-client contacts ``` -When prompted for the `ak`, paste the `verifying_key` value that was printed in -the previous part, inside the Public Key Package. For example, in the following -package +Then run the following command, replacing the `` and `` hex +strings with the public keys of the contacts which will participate (along with +the user running the command): ``` -Public key package: -{"verifying_shares": ...snip... ,"verifying_key":"d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b","ciphersuite":"FROST(Pallas, BLAKE2b-512)"} +frost-client dkg -d "Alice, Bob and Eve's group" -s localhost:2744 -S , -t 2 -C redpallas -c alice.toml ``` -you would need to use -`d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b`. Press -enter to submit. +The user should then notify the others that a signing session has started (e.g. +via instant messaging again), and also share the threshold number that was used. +They should then run the following, replacing the name of the group if they wish +and the threshold number with the one given by the first participant. + +``` +frost-client dkg -d "Alice, Bob and Eve's group" -s localhost:2744 -t 2 -C redpallas +``` + +```admonish note +A future version might not require specifying the threshold and group name. +``` + + +## Generating the Full Viewing Key for the wallet + +Next, we will need to generate a Zcash Full Viewing Key from the FROST group +material we have just generated; this address will then be imported into a wallet +so that we'll be able to create Zcash transactions for it. + +Run the following command: + +``` +frost-client groups +``` + +It will list all groups you're in - at this point it should list the only one +you have just created. Copy the Public Key it shows (it will look like e.g. +`79d6bcee79c88ad9ba259067772b97f5de12f1435b474d03bc98f255be08a610`) + +The run the following command, replacing `` with the value you copied. + +``` +zcash-sign generate --ak --danger-dummy-sapling +``` It will print an Orchard address, and a Unified Full Viewing Key. Copy and paste both somewhere to use them later. + ## Importing the Full Viewing Key into Ywallet Open Ywallet and click "New account". Check "Restore an account" and paste the Unified Full Viewing Key created in the previous step. Click "Import". -In the "Rescan from..." window, pick today's date (since the wallet was just -created) and press OK. The wallet should open. - -You will need to change some of Ywallet configurations. Click the three dots -at the top right and go to Settings. Switch to Advanced mode and click -OK. Go back to the Settings and uncheck "Use QR for offline signing". ## Funding the wallet Now you will need to fund this wallet with some ZEC. Use the Orchard address printed by the signer (see warning below). Send ZEC to that address using -another account (or try [ZecFaucet](https://zecfaucet.com/)). Wait until the -funds become spendable (this may take ~10 minutes). You can check if the funds -are spendable by clicking the arrow button and checking "Spendable Balance" - -```admonish warning -The address being show by Ywallet is a unified address that includes both an Orchard and Sapling address. For the demo to work, you need to receive funds in you Orchard address. Whether that will happen depends on multiple factors so it's probably easier to use just the Orchard-only address printed by the signer. +another account (or try [ZecFaucet](https://zecfaucet.com/)). + +```admonish danger +The address being show by Ywallet is a unified address that includes both an +Orchard and Sapling address. For the demo to work, you need to receive funds in +your Orchard address. Whether that will happen depends on multiple factors so +it's probably easier to use just the Orchard-only address printed by the signer. +In Ywallet, you can also swipe right on the QR Code until it shows the "Orchard +Address". **IF YOU SEND IT TO THE SAPLING ADDRESS, THE FUNDS WILL BECOME +UNSPENDABLE AND WILL BE LOST!** ``` + ## Creating the transaction Now you will create the transaction that you wish to sign with FROST. Click the arrow button and paste the destination address (send it to yourself if you don't know where to send it). Type the amount you want to send and -click "Send". +click the arrow button. + +The wallet will show the transaction plan. Click the snowflake button. It will +show a QR code, but we want that information as a file, so click the floppy disk +button and save the file somewhere (e.g. `tx-plan.json`). -The wallet will show the transaction plan. Click "Send". It won't actually -send - it will prompt you for where to save the transaction plan. Save it -somewhere. ## Signing the transaction -Go back to the signer terminal and run (adjust paths accordingly. The "tx.json" -input parameters must point to the file you save in the previous step, and the -"tx.raw" output parameter is where the signed transaction will be written). +Now you will need to simulate two participants and a Coordinator to sign the +transaction, and you should still have the FROST server running which will +handle communications between them. It's probably easier to open three new +terminals. + +Go back to the signer terminal and run the following, replacing `` +with the path to the file you saved in the previous step, `` with the UFVK +hex string printed previously, and `` with the path where you +want to write the signed transaction (e.g. `tx-signed.raw`). ``` -cargo run --release --bin sign --features dotenv -- -t ~/Downloads/tx.json -o ~/Downloads/tx.raw +zcash-sign sign --tx-plan --ufvk -o ``` -When prompted, paste the UFVK generated previously. +The program will print a SIGHASH and a Randomizer, and will prompt for a +signature. This is what you will get after running FROST, so let's do that; +leave the prompt there without typing anything. -The program will print a SIGHASH and a Randomizer. -Now you will need to simulate two participants and a Coordinator to sign the -transaction. It's probably easier to open three terminals. +### Coordinator -In the first one, the Coordinator, run (in the same folder where key -generation was run): +In the second terminal, the Coordinator, run (in the same folder where you +initialized the users and ran the key generation) the following: ``` -cargo run --bin coordinator --features redpallas +frost-client groups -c alice.toml ``` -And then: +This will list the groups Alice is in; it should only list the one you created +earlier. You will need to copy some values in the command. Run the following, +replacing the value after `` with the "Public key" listed for the group; +replacing `` and `` with the public keys of Alice and Bob (the +hexadecimal values printed next to their names; Alice's name will be empty to +indicate it's her own). -- Paste the JSON public key package generate during key generation (it's a single - line with a JSON object). -- Type `2` for the number of participants. -- Paste the identifier of the first participant, you can see it in the Secret - Share printed during key generation. If you used trusted dealer key - generation, it will be - `0100000000000000000000000000000000000000000000000000000000000000`. -- Paste the second identifier, e.g. - `0200000000000000000000000000000000000000000000000000000000000000`. -- When prompted for the message to be signed, paste the SIGHASH printed by the - signer above (just the hex value, e.g. - ``4d065453cfa4cfb4f98dbc9cff60c4a3904ed91c523b8ef8d67d28bea7f12ea3``). +``` +frost-client coordinator -c alice.toml --server-url localhost:2744 --group -S , -m - -r - +``` -Create a new terminal, for participant 1, and run: +It will prompt you for a message. Paste the SIGHASH generated with the +`zcash-sign` tool and press enter. It will then prompt for a randomizer. Paste +the one generated with the `zcash-sign` tool and press enter. +The tool will connect to the server and wait for the other participants. + +```admonish warning +If you prefer to pass the message (SIGHASH) or randomizer as files by using +the `-m` and `-r` arguments, you will need to convert them to binary format. ``` -cargo run --bin participant --features redpallas + + +### Participant 1 (Alice) + +In the third terminal, Participant 1, run the following (replacing `` +with the same group public key used in the previous command): + +``` +frost-client participant -c alice.toml --server-url localhost:2744 --group ``` -And then: +(We are using "Alice" again. There's nothing stopping a Coordinator from being a +Partcipant too!) -- Paste the Secret Share printed during key generation (or Key Package if you - used DKG). -- Copy the SigningCommitments line and paste into the Coordinator CLI. -Do the same for participant 2. +### Participant 2 (Bob) -You should be at the Coordinator CLI. Paste the Randomizer generated by the -signer before and copy the Signing Package line that it was printed by the -Coordinator CLI before the Randomizer prompt. +In the fourth terminal, for Participant 2, run the following (replacing `` +again): -Switch to participant 1 and: +``` +frost-client participant -c bob.toml --server-url localhost:2744 --group +``` -- Paste the Signing Package -- Paste the Randomizer printed by the signer before. -- Copy the SignatureShare line and paste it into the Coordinator CLI. -Do the same for participant 2. +### Coordinator -You should be at the Coordinator CLI. It has just printed the final -FROST-generated signature. Hurrah! Copy it (just the hex value). +Go back to the Coordinator CLI. The protocol should run and complete +successfully. It will print the final FROST-generated signature. Hurrah! Copy it +(just the hex value). Go back to the signer and paste the signature. It will write the raw signed transaction to the file you specified. + ## Broadcasting the transaction -Go back to Ywallet and return to its main screen. In the menu, select "Advanced" -and "Broadcast". Select the raw signed transaction file you have just generated. +Go back to Ywallet and return to its main screen. In the menu, select "More" and +"Broadcast". Click the upper-right box-with-an-arrow icon and select the raw +signed transaction file you have just generated (`tx-signed.raw` if you followed +the suggestion). That's it! You just sent a FROST-signed Zcash transaction. diff --git a/frost-core/CHANGELOG.md b/frost-core/CHANGELOG.md index 4821c4752..373eb4470 100644 --- a/frost-core/CHANGELOG.md +++ b/frost-core/CHANGELOG.md @@ -2,9 +2,161 @@ Entries are listed in reverse chronological order. + ## Unreleased -## Released + +## 3.0.0-rc.0 + +### Breaking Changes + +* The `cheater-detection` feature was removed. If you relied on it (either by + using the default features, or by explicitly enabling it), then you don't have + to do anything (other than not enabling it explicitly if you were doing so); + the default behaviour is now as if `cheater-detection` was enabled. If you + explicitly *did not enable* it, you can avoid cheater detection by calling + `aggregate_custom()` with `CheaterDetection::Disabled`. +* Changed `InvalidSignatureShare::culprit` to `culprits`; it is now a `Vec`. +* Changed `Error::culprit()` to `culprits()`; is is now a `Vec`. +* Added a `min_signers` argument to `PublicKeyPackage::new()`. +* The `std` and `nightly` features were removed from all crates. +* Renamed `frost_core::keys::refresh::refresh_dkg_part_1` to `refresh_dkg_part1`. +* Fixed the crate-specific versions of the `refresh` module to be non-generic. +* Removed the `min_signers` and `max_signers` arguments from + `frost_core::keys::refresh::compute_refreshing_shares()`. The former is now + read from the `pub_key_package`; if you pass a pre-3.0.0 generate package, + you will need to fills its `min_signers` field with the original threshold + before calling the function (recreate it with `PublicKeyPackage::new()`). + The latter was simply redundant. +* `SigningKey` is no longer `Copy`; and now it implements `ZeroizeOnDrop`. +* Refactored the `frost_core::keys::repairable` module: + * `repair_share_step_1()` was renamed to `repair_share_part1()` and now takes + a `KeyPackage` and returns a map with a new `Delta` type instead of a raw + `Scalar` + * `repair_share_step_2()` was renamed to `repair_share_part2()` and now takes + the `Delta` type and returns a new `Sigma` type instead of a raw `Scalar` + * `repair_share_step_3()` was renamed to `repair_share_part3()` and now takes + the `Sigma` type and a `PublicKeyPackage` instead of + `VerifiableSecretSharingCommitment`; and returns a `KeyPackage` instead of + `SecretShare`. + * These changes provide more type safety and are make it more useful since + `SecretPackage`s are not expected to be stored. +* The `Ciphersuite`, `Scalar` and `Element` traits now must implement `Send` and + `Sync`. This should be trivial in most cases. +* The `SignatureSerialization`, `Field::Serialization` and + `Element::Serialization` traits do not need to implement `TryFrom>` + anymore; instead, they must implement `AsMut<[u8]>` and `TryFrom<&[u8]>`. This + should be trivial in most cases since they are often implemented by arrays. + +### Additional changes + +* Added DKG refresh functions to the crate-specific `refresh` modules. +* Re-exported the `frost-rerandomized` crate in the ciphersuite functions, e.g. + you can call `frost_ristretto255::rerandomized::sign_with_randomizer_seed()`. +* Added the `pre_commitment_aggregate()` and `pre_commitment_sign()` hooks + to the `Ciphersuite` trait. +* Added `aggregate_custom()` function to allow specifying which cheater + detection strategy to use. The original `aggregate()` behaviour is to use + the `CheaterDetection::FirstCheater` strategy. + + +## 2.2.0 + +### Security Fixes + +* Added validation for the `min_signers` parameter in the + `frost_core::keys::refresh` functions. It was not clear that it is not + possible to change `min_signers` with the refresh procedure. Using a smaller + value would not decrease the threshold, and attempts to sign using a smaller + threshold would fail. Additionally, after refreshing the shares with a smaller + threshold, it would still be possible to sign with the original threshold; + however, this could cause a security loss to the participant's shares. We have + not determined the exact security implications of doing so and judged simpler + to just validate `min_signers`. If for some reason you have done a refresh + share procedure with a smaller `min_signers` we strongly recommend migrating + to a new key. Thank you [BlockSec](https://blocksec.com/) for reporting the + finding. + +### Other Changes + +* MSRV has been bumped to Rust 1.81, making all crates no-std (except + `frost-ed448`). +* Added DKG refresh functions to the crate-specific `refresh` modules. +* Added `VerifiableSecretSharingCommitment::{serialize,deserialize}_whole()` + methods. +* Added `Ciphersuite::post_generate()` method to allow more ciphersuite + customization. + +## 2.1.0 + +* It is now possible to identify the culprit in `frost_core::keys::dkg::part3()` + if an invalid secret share was sent by one of the participants (by calling + `frost_core::Error::culprit()`) (#728) +* Added frost-secp256k1-tr crate, allowing to generate Bitcoin Taproot + (BIP340/BIP341) compatible signatures (#730). +* Support refreshing shares using the DKG approach using the + `frost_core::keys::refresh::refresh_dkg_{part1,part2,shares}()` functions + (#766). +* `frost_core::keys::dkg::part{1,2}::SecretPackage` are now serializable (#833). + +## 2.0.0 + +* Updated docs +* Added missing `derive(Getters)` for `dkg::{round1, round2}` +* Added `internal` feature for `validate_num_of_signers` +* Added refresh share functionality for trusted dealer: + `frost_core::keys::refresh::{compute_refreshing_shares, refresh_share}` +* Added a `'static` bound to the `Ciphersuite` trait. This is a breaking change, + but it's likely to not require any code changes since most ciphersuite + implementations are probably just empty structs. The bound makes it possible + to use `frost_core::Error` in `Box`. +* Added getters to `round1::SecretPackage` and `round2::SecretPackage`. +* Added a `frost_core::verify_signature_share()` function which allows verifying + individual signature shares. This is not required for regular FROST usage but + might useful in certain situations where it is desired to verify each + individual signature share before aggregating the signature. + +## 2.0.0-rc.0 + +* Changed the `deserialize()` function of Elements and structs containing + Elements to return an error if the element is the identity. This is a + requirement in the FROST specification that wasn't being followed. We are not + aware of any possible security issues that could be caused by this; in the + unlikely case that the identity was being serialized, this would be caught by + deserialization methods. However, we consider this change the right thing to + do as a defense-in-depth mechanism. This entails the following changes: + * `Group::serialize()` now returns an error. When implementing it, you must + return an error if it attempts to serialize the identity. + * `VerifyingShare::serialize()`, `CoefficientCommitment::serialize()`, + `VerifiableSecretSharingCommitment::serialize()`, + `NonceCommitment::serialize()`, `Signature::serialize()`, + `VerifyingKey::serialize()` can now all return an error. +* Changed the `serialize()` and `deserialize()` methods of all Scalar- and + Element-wrapping structs; instead of taking or returning a + `Field::Serialization` or `Element::Serialization` trait (which are usually + defined by ciphersuites as arrays of specific sizes), they simply respectively + take `&[u8]` and return `Vec`, exactly as the other structs, which should + greatly simplify non-serde serialization code. You can port existing code with + e.g. `x.serialize().as_ref()` -> `x.serialize()` and + `X::deserialize(bytes.try_into().unwrap())` -> `X::deserialize(&bytes)`. +* Removed the `ops::{Mul, MulAssign, Sub}` implementation for `Identifier`. + These were being used internally, but library users shouldn't need to use them. + If you have low-level code that relied on it, use `Identifier::{new, + to_scalar}` to handle the underlying scalar. +* Removed `batch::Item::into()` which created a batch Item from a triple of + VerifyingKey, Signature and message. Use the new `batch::Item::new()` instead + (which can return an error). +* Add no-std support to all crates except frost-ed448. To use, do not enable the + `std` feature that is enabled by default (i.e. use `default-features = + false`); Note that it always links to an external `alloc` crate (i.e. there is + no `alloc` feature). When disabling `std`, the only impact in the API is that + `Error` will no longer implement the `std::error::Error` trait. This is a + breaking change if you are disabling default features but rely on `Error` + implementing `std::error::Error`. In that case, simply enable the `std` + feature. +* Fixed `no-default-features`, previously it wouldn't compile. +* Fixed some feature handling that would include unneeded dependencies in some + cases. ## 1.0.0 diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index 0dcaa8b61..27642bf41 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -1,19 +1,13 @@ [package] name = "frost-core" -edition = "2021" -# When releasing to crates.io: -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = [ - "Deirdre Connolly ", - "Chelsea Komlo ", - "Conrado Gouvea ", -] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] +license.workspace = true +repository.workspace = true +categories.workspace = true keywords = ["cryptography", "crypto", "threshold", "signature", "schnorr"] description = "Types and traits to support implementing Flexible Round-Optimized Schnorr Threshold signature schemes (FROST)." @@ -22,35 +16,43 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -byteorder = "1.4" -const-crc32 = "1.2.0" -document-features = "0.2.7" -debugless-unwrap = "0.0.4" -derive-getters = "0.3.0" -hex = "0.4.3" -postcard = { version = "1.0.0", features = ["use-std"], optional = true } -rand_core = "0.6" -serde = { version = "1.0.160", features = ["derive"], optional = true } +byteorder = { version = "1.4", default-features = false } +const-crc32 = { version = "1.2.0", package = "const-crc32-nostd" } +document-features.workspace = true +debugless-unwrap = { version = "1.0.0", optional = true } +derive-getters = "0.5.0" +hex.workspace = true +postcard = { version = "1.0.0", features = ["alloc"], optional = true } +rand_core = { version = "0.6", default-features = false } +serde = { version = "1.0.160", default-features = false, features = ["derive"], optional = true } serdect = { version = "0.2.0", optional = true } -thiserror = "1.0" +thiserror = { version = "2.0.3", default-features = false } visibility = "0.1.0" -zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } -itertools = "0.12.0" +zeroize = { version = "1.5.4", default-features = false, features = ["derive", "alloc"] } +# We indirectly depend on this via `zeroize` but the minimal version enforced by +# `zeroize` does not work for us, so we specify it here. +zeroize_derive = { version = "1.4.2" } +itertools = { version = "0.14.0", default-features = false } # Test dependencies used with the test-impl feature proptest = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true } -criterion = { version = "0.5", optional = true } +criterion = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } [dev-dependencies] -lazy_static = "1.4" -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +debugless-unwrap = "1.0.0" +criterion.workspace = true +lazy_static.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true + [features] -default = ["serialization", "cheater-detection"] +default = ["serialization"] #! ## Features ## Expose internal types, which do not have SemVer guarantees. This is an advanced ## feature which can be useful if you need to build a modified version of FROST. @@ -62,9 +64,7 @@ internals = [] serde = ["dep:serde", "dep:serdect"] serialization = ["serde", "dep:postcard"] # Exposes ciphersuite-generic tests for other crates to use -test-impl = ["proptest", "serde_json", "criterion"] -# Enable cheater detection -cheater-detection = [] +test-impl = ["dep:proptest", "dep:serde_json", "dep:criterion", "dep:tokio", "dep:debugless-unwrap"] [lib] bench = false diff --git a/frost-core/README.md b/frost-core/README.md index aac4cf268..2fe7e6833 100644 --- a/frost-core/README.md +++ b/frost-core/README.md @@ -1,30 +1,11 @@ # FROST (Flexible Round-Optimised Schnorr Threshold signatures) Core Base traits and types in Rust that implement ['Two-Round Threshold Schnorr Signatures with -FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) generically for +FROST'](https://datatracker.ietf.org/doc/rfc9591/) generically for [`Ciphersuite`] implementations. -For key generation, refer to the [`keys`] module. For round-specific -types and functions, refer to the [`round1`] and [`round2`] modules. This module -contains types and functions not directly related to key generation and the -FROST rounds. - - -## Status ⚠ - -The FROST specification is not yet finalized, though no significant changes are -expected at this point. This code base has been audited by NCC. The APIs and -types in `frost-core` are subject to change during the release candidate phase, -and will follow SemVer guarantees after 1.0.0. - -## Usage - -`frost-core` implements the base traits and types in a generic manner, to enable top-level -implementations for different ciphersuites / curves without having to implement all of FROST from -scratch. End-users should not use `frost-core` if they want to sign and verify signatures, they -should use the crate specific to their ciphersuite/curve parameters that uses `frost-core` as a -dependency, such as [`frost_ristretto255`](../frost_ristretto255). +For more details, refer to [The ZF FROST Book](https://frost.zfnd.org/). ## Example -See ciphersuite-specific crates, e.g. [`frost_ristretto255`](../frost_ristretto255). +See ciphersuite-specific crates, e.g. [`frost_ristretto255`](https://crates.io/crates/frost-ristretto255). diff --git a/frost-core/src/batch.rs b/frost-core/src/batch.rs index 37d25bdfe..a30a109e4 100644 --- a/frost-core/src/batch.rs +++ b/frost-core/src/batch.rs @@ -7,8 +7,6 @@ //! of caller code (which must assemble a batch of signatures across //! work-items), and loss of the ability to easily pinpoint failing signatures. -use std::iter::once; - use rand_core::{CryptoRng, RngCore}; use crate::{scalar_mul::VartimeMultiscalarMul, Ciphersuite, Element, *}; @@ -25,16 +23,24 @@ pub struct Item { c: Challenge, } -impl<'msg, C, M> From<(VerifyingKey, Signature, &'msg M)> for Item +impl Item where C: Ciphersuite, - M: AsRef<[u8]>, { - fn from((vk, sig, msg): (VerifyingKey, Signature, &'msg M)) -> Self { - // Compute c now to avoid dependency on the msg lifetime. - let c = crate::challenge(&sig.R, &vk, msg.as_ref()); - - Self { vk, sig, c } + /// Create a new batch [`Item`] from a [`VerifyingKey`], [`Signature`] + /// and a message to be verified. + pub fn new(vk: VerifyingKey, sig: Signature, msg: M) -> Result> + where + M: AsRef<[u8]>, + { + let (msg, sig, vk) = ::pre_verify(msg.as_ref(), &sig, &vk)?; + let c = ::challenge(&sig.R, &vk, &msg)?; + + Ok(Self { + vk: *vk, + sig: *sig, + c, + }) } } @@ -129,10 +135,10 @@ where Rs.push(R); VK_coeffs.push(<::Field>::zero() + (blind * item.c.0)); - VKs.push(item.vk.element); + VKs.push(item.vk.to_element()); } - let scalars = once(&P_coeff_acc) + let scalars = core::iter::once(&P_coeff_acc) .chain(VK_coeffs.iter()) .chain(R_coeffs.iter()); @@ -155,6 +161,8 @@ where C: Ciphersuite, { fn default() -> Self { - Self { signatures: vec![] } + Self { + signatures: Vec::new(), + } } } diff --git a/frost-core/src/benches.rs b/frost-core/src/benches.rs index 74931bc75..fd4ea7313 100644 --- a/frost-core/src/benches.rs +++ b/frost-core/src/benches.rs @@ -1,11 +1,13 @@ //! Ciphersuite-generic benchmark functions. #![allow(clippy::unwrap_used)] -use std::collections::BTreeMap; +use core::iter; -use criterion::{BenchmarkId, Criterion, Throughput}; +use alloc::{collections::BTreeMap, format, vec::Vec}; use rand_core::{CryptoRng, RngCore}; +use criterion::{BenchmarkId, Criterion, Throughput}; + use crate as frost; use crate::{batch, Ciphersuite, Signature, SigningKey, VerifyingKey}; @@ -18,7 +20,7 @@ fn sigs_with_distinct_keys( rng: &mut R, ) -> impl Iterator> { let mut rng = rng.clone(); - std::iter::repeat_with(move || { + iter::repeat_with(move || { let msg = b"Bench"; let sk = SigningKey::new(&mut rng); let vk = VerifyingKey::from(&sk); @@ -67,7 +69,8 @@ pub fn bench_batch_verify( let msg = b"Bench"; let Item { vk, sig } = item; - batch.queue((*vk, *sig, msg)); + let item = batch::Item::::new(*vk, *sig, msg).unwrap(); + batch.queue(item); } batch.verify(&mut rng) }) @@ -86,6 +89,8 @@ pub fn bench_sign( let mut group = c.benchmark_group(format!("FROST Signing {name}")); for &n in [3u16, 10, 100, 1000].iter() { let max_signers = n; + // div_ceil is in 1.73.0 which is larger than the current MSRV + #[allow(clippy::manual_div_ceil)] let min_signers = (n * 2 + 2) / 3; group.bench_with_input( diff --git a/frost-core/src/error.rs b/frost-core/src/error.rs index 3a768badf..e11d9fd54 100644 --- a/frost-core/src/error.rs +++ b/frost-core/src/error.rs @@ -1,15 +1,12 @@ //! FROST Error types -use thiserror::Error; - use crate::{Ciphersuite, Identifier}; - -#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)] -pub struct ParticipantError(Identifier); +use alloc::vec::Vec; +use thiserror::Error; /// An error related to FROST. #[non_exhaustive] -#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Error, Debug, Clone, Eq, PartialEq)] pub enum Error { /// min_signers is invalid #[error("min_signers must be at least 2 and not larger than max_signers")] @@ -66,11 +63,15 @@ pub enum Error { #[error("Invalid signature share.")] InvalidSignatureShare { /// The identifier of the signer whose share validation failed. - culprit: Identifier, + culprits: Vec>, }, /// Secret share verification failed. #[error("Invalid secret share.")] - InvalidSecretShare, + InvalidSecretShare { + /// The identifier of the signer whose secret share validation failed, + /// if possible to identify. + culprit: Option>, + }, /// Round 1 package not found for Round 2 participant. #[error("Round 1 package not found for Round 2 participant.")] PackageNotFound, @@ -113,23 +114,19 @@ impl Error where C: Ciphersuite, { - /// Return the identifier of the participant that caused the error. - /// Returns None if not applicable for the error. + /// Return the identifiers of the participants that caused the error. + /// Returns an empty vector if not applicable for the error. /// - /// This can be used to penalize a participant that does not follow the + /// This can be used to penalize participants that do not follow the /// protocol correctly, e.g. removing them from further signings. - pub fn culprit(&self) -> Option> { + pub fn culprits(&self) -> Vec> { // Use an exhaustive match to make sure that if we add new enum items // then we will explicitly check if they should be added here. match self { - Error::InvalidSignatureShare { - culprit: identifier, - } - | Error::InvalidProofOfKnowledge { - culprit: identifier, - } => Some(*identifier), - Error::InvalidSecretShare - | Error::InvalidMinSigners + Error::InvalidSignatureShare { culprits } => culprits.clone(), + Error::InvalidProofOfKnowledge { culprit } => vec![*culprit], + Error::InvalidSecretShare { culprit } => culprit.map(|i| vec![i]).unwrap_or_default(), + Error::InvalidMinSigners | Error::InvalidMaxSigners | Error::InvalidCoefficients | Error::MalformedIdentifier @@ -155,7 +152,7 @@ where | Error::IncorrectNumberOfCommitments | Error::SerializationError | Error::DeserializationError - | Error::IdentifierDerivationNotSupported => None, + | Error::IdentifierDerivationNotSupported => vec![], } } } diff --git a/frost-core/src/identifier.rs b/frost-core/src/identifier.rs index 5525197bf..9dc4e8fc8 100644 --- a/frost-core/src/identifier.rs +++ b/frost-core/src/identifier.rs @@ -1,14 +1,15 @@ //! FROST participant identifiers -use std::{ +use core::{ fmt::{self, Debug}, hash::{Hash, Hasher}, }; -use crate::{Ciphersuite, Error, Field, FieldError, Group, Scalar}; +use alloc::vec::Vec; -#[cfg(feature = "serde")] -use crate::serialization::ScalarSerialization; +use crate::{ + serialization::SerializableScalar, Ciphersuite, Error, Field, FieldError, Group, Scalar, +}; /// A FROST participant identifier. /// @@ -18,23 +19,34 @@ use crate::serialization::ScalarSerialization; #[derive(Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ScalarSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ScalarSerialization"))] -pub struct Identifier(Scalar); +// We use these to add a validation step since zero scalars should cause an +// error when deserializing. +#[cfg_attr(feature = "serde", serde(try_from = "SerializableScalar"))] +#[cfg_attr(feature = "serde", serde(into = "SerializableScalar"))] +pub struct Identifier(SerializableScalar); impl Identifier where C: Ciphersuite, { /// Create a new Identifier from a scalar. For internal use only. - fn new(scalar: Scalar) -> Result> { + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Result> { if scalar == <::Field>::zero() { Err(FieldError::InvalidZeroScalar.into()) } else { - Ok(Self(scalar)) + Ok(Self(SerializableScalar(scalar))) } } + /// Get the inner scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 + } + /// Derive an Identifier from an arbitrary byte string. /// /// This feature is not part of the specification and is just a convenient @@ -50,39 +62,36 @@ where } /// Serialize the identifier using the ciphersuite encoding. - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.0) + pub fn serialize(&self) -> Vec { + self.0.serialize() } /// Deserialize an Identifier from a serialized buffer. /// Returns an error if it attempts to deserialize zero. - pub fn deserialize( - buf: &<::Field as Field>::Serialization, - ) -> Result> { - let scalar = <::Field>::deserialize(buf)?; - Self::new(scalar) + pub fn deserialize(bytes: &[u8]) -> Result> { + Self::new(SerializableScalar::deserialize(bytes)?.0) } } #[cfg(feature = "serde")] -impl TryFrom> for Identifier +impl TryFrom> for Identifier where C: Ciphersuite, { type Error = Error; - fn try_from(value: ScalarSerialization) -> Result { - Self::deserialize(&value.0) + fn try_from(s: SerializableScalar) -> Result { + Self::new(s.0) } } #[cfg(feature = "serde")] -impl From> for ScalarSerialization +impl From> for SerializableScalar where C: Ciphersuite, { - fn from(value: Identifier) -> Self { - Self(value.serialize()) + fn from(i: Identifier) -> Self { + i.0 } } @@ -94,9 +103,7 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("Identifier") - .field(&hex::encode( - <::Field>::serialize(&self.0).as_ref(), - )) + .field(&hex::encode(self.serialize())) .finish() } } @@ -107,9 +114,7 @@ where C: Ciphersuite, { fn hash(&self, state: &mut H) { - <::Field>::serialize(&self.0) - .as_ref() - .hash(state) + self.serialize().hash(state) } } @@ -117,9 +122,11 @@ impl Ord for Identifier where C: Ciphersuite, { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let serialized_self = <::Field>::little_endian_serialize(&self.0); - let serialized_other = <::Field>::little_endian_serialize(&other.0); + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let serialized_self = + <::Field>::little_endian_serialize(&self.to_scalar()); + let serialized_other = + <::Field>::little_endian_serialize(&other.to_scalar()); // The default cmp uses lexicographic order; so we need the elements in big endian serialized_self .as_ref() @@ -133,42 +140,11 @@ impl PartialOrd for Identifier where C: Ciphersuite, { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl std::ops::Mul> for Identifier -where - C: Ciphersuite, -{ - type Output = Scalar; - - fn mul(self, scalar: Scalar) -> Scalar { - self.0 * scalar - } -} - -impl std::ops::MulAssign> for Scalar -where - C: Ciphersuite, -{ - fn mul_assign(&mut self, identifier: Identifier) { - *self = *self * identifier.0 - } -} - -impl std::ops::Sub for Identifier -where - C: Ciphersuite, -{ - type Output = Self; - - fn sub(self, rhs: Identifier) -> Self::Output { - Self(self.0 - rhs.0) - } -} - impl TryFrom for Identifier where C: Ciphersuite, @@ -191,7 +167,7 @@ where sum = sum + one; } } - Ok(Self(sum)) + Self::new(sum) } } } diff --git a/frost-core/src/keys.rs b/frost-core/src/keys.rs index 533cb13f8..5389f5ebc 100644 --- a/frost-core/src/keys.rs +++ b/frost-core/src/keys.rs @@ -1,12 +1,15 @@ //! FROST keys, keygen, key shares #![allow(clippy::type_complexity)] +// Remove after https://github.com/rust-lang/rust/issues/147648 is fixed +#![allow(unused_assignments)] -use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, - convert::TryFrom, - default::Default, +use core::iter; + +use alloc::{ + collections::{BTreeMap, BTreeSet}, fmt::{self, Debug}, - iter, + string::ToString, + vec::Vec, }; use derive_getters::Getters; @@ -14,20 +17,21 @@ use derive_getters::Getters; use hex::FromHex; use rand_core::{CryptoRng, RngCore}; -use zeroize::{DefaultIsZeroes, Zeroize}; +use zeroize::{DefaultIsZeroes, Zeroize, ZeroizeOnDrop}; use crate::{ - serialization::{Deserialize, Serialize}, + serialization::{SerializableElement, SerializableScalar}, Ciphersuite, Element, Error, Field, Group, Header, Identifier, Scalar, SigningKey, VerifyingKey, }; -#[cfg(feature = "serde")] -use crate::serialization::{ElementSerialization, ScalarSerialization}; +#[cfg(feature = "serialization")] +use crate::serialization::{Deserialize, Serialize}; use super::compute_lagrange_coefficient; pub mod dkg; +pub mod refresh; pub mod repairable; /// Sum the commitments from all participants in a distributed key generation @@ -37,7 +41,7 @@ pub(crate) fn sum_commitments( commitments: &[&VerifiableSecretSharingCommitment], ) -> Result, Error> { let mut group_commitment = vec![ - CoefficientCommitment(::identity()); + CoefficientCommitment::new(::identity()); commitments .first() .ok_or(Error::IncorrectNumberOfCommitments)? @@ -46,7 +50,7 @@ pub(crate) fn sum_commitments( ]; for commitment in commitments { for (i, c) in group_commitment.iter_mut().enumerate() { - *c = CoefficientCommitment( + *c = CoefficientCommitment::new( c.value() + commitment .0 @@ -81,44 +85,41 @@ pub(crate) fn default_identifiers(max_signers: u16) -> Vec(pub(crate) Scalar); +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct SigningShare(pub(crate) SerializableScalar); impl SigningShare where C: Ciphersuite, { /// Create a new [`SigningShare`] from a scalar. - #[cfg(feature = "internals")] - pub fn new(scalar: Scalar) -> Self { - Self(scalar) + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Self { + Self(SerializableScalar(scalar)) } /// Get the inner scalar. - #[cfg(feature = "internals")] - pub fn to_scalar(&self) -> Scalar { - self.0 + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 } /// Deserialize from bytes - pub fn deserialize( - bytes: <::Field as Field>::Serialization, - ) -> Result> { - <::Field>::deserialize(&bytes) - .map(|scalar| Self(scalar)) - .map_err(|e| e.into()) + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) } /// Serialize to bytes - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.0) + pub fn serialize(&self) -> Vec { + self.0.serialize() } /// Computes the signing share from a list of coefficients. #[cfg_attr(feature = "internals", visibility::make(pub))] pub(crate) fn from_coefficients(coefficients: &[Scalar], peer: Identifier) -> Self { - Self(evaluate_polynomial(peer, coefficients)) + Self::new(evaluate_polynomial(peer, coefficients)) } } @@ -126,7 +127,7 @@ impl Debug for SigningShare where C: Ciphersuite, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("SigningShare").field(&"").finish() } } @@ -136,7 +137,7 @@ where C: Ciphersuite, { fn default() -> Self { - Self(<::Field>::zero()) + Self::new(<::Field>::zero()) } } @@ -152,32 +153,7 @@ where fn from_hex>(hex: T) -> Result { let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; - match v.try_into() { - Ok(bytes) => Self::deserialize(bytes).map_err(|_| "malformed secret encoding"), - Err(_) => Err("malformed secret encoding"), - } - } -} - -#[cfg(feature = "serde")] -impl TryFrom> for SigningShare -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ScalarSerialization) -> Result { - Self::deserialize(value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ScalarSerialization -where - C: Ciphersuite, -{ - fn from(value: SigningShare) -> Self { - Self(value.serialize()) + Self::deserialize(&v).map_err(|_| "malformed scalar") } } @@ -185,9 +161,8 @@ where #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ElementSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ElementSerialization"))] -pub struct VerifyingShare(pub(super) Element) +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct VerifyingShare(pub(super) SerializableElement) where C: Ciphersuite; @@ -196,27 +171,28 @@ where C: Ciphersuite, { /// Create a new [`VerifyingShare`] from a element. - #[cfg(feature = "internals")] - pub fn new(element: Element) -> Self { - Self(element) + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(element: Element) -> Self { + Self(SerializableElement(element)) } /// Get the inner element. - #[cfg(feature = "internals")] - pub fn to_element(&self) -> Element { - self.0 + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(dead_code)] + pub(crate) fn to_element(&self) -> Element { + self.0 .0 } /// Deserialize from bytes - pub fn deserialize(bytes: ::Serialization) -> Result> { - ::deserialize(&bytes) - .map(|element| Self(element)) - .map_err(|e| e.into()) + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) } /// Serialize to bytes - pub fn serialize(&self) -> ::Serialization { - ::serialize(&self.0) + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() } /// Computes a verifying share for a peer given the group commitment. @@ -236,7 +212,7 @@ where // Y_i = ∏_{k=0}^{t−1} (∏_{j=1}^n φ_{jk})^{i^k mod q} // i.e. we can operate on the sum of all φ_j commitments, which is // what is passed to the functions. - VerifyingShare(evaluate_vss(identifier, commitment)) + VerifyingShare::new(evaluate_vss(identifier, commitment)) } } @@ -246,7 +222,12 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("VerifyingShare") - .field(&hex::encode(self.serialize())) + .field( + &self + .serialize() + .map(hex::encode) + .unwrap_or("".to_string()), + ) .finish() } } @@ -256,29 +237,7 @@ where C: Ciphersuite, { fn from(secret: SigningShare) -> VerifyingShare { - VerifyingShare(::generator() * secret.0 as Scalar) - } -} - -#[cfg(feature = "serde")] -impl TryFrom> for VerifyingShare -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ElementSerialization) -> Result { - Self::deserialize(value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ElementSerialization -where - C: Ciphersuite, -{ - fn from(value: VerifyingShare) -> Self { - Self(value.serialize()) + VerifyingShare::new(::generator() * secret.to_scalar()) } } @@ -289,9 +248,7 @@ where #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ElementSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ElementSerialization"))] -pub struct CoefficientCommitment(pub(crate) Element); +pub struct CoefficientCommitment(pub(crate) SerializableElement); impl CoefficientCommitment where @@ -300,24 +257,22 @@ where /// Create a new CoefficientCommitment. #[cfg_attr(feature = "internals", visibility::make(pub))] pub(crate) fn new(value: Element) -> Self { - Self(value) + Self(SerializableElement(value)) } - /// returns serialized element - pub fn serialize(&self) -> ::Serialization { - ::serialize(&self.0) + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) } - /// Creates a new commitment from a coefficient input - pub fn deserialize( - coefficient: ::Serialization, - ) -> Result, Error> { - Ok(Self::new(::deserialize(&coefficient)?)) + /// Serialize to bytes + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() } /// Returns inner element value pub fn value(&self) -> Element { - self.0 + self.0 .0 } } @@ -325,35 +280,18 @@ impl Debug for CoefficientCommitment where C: Ciphersuite, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("CoefficientCommitment") - .field(&hex::encode(self.serialize())) + .field( + &self + .serialize() + .map(hex::encode) + .unwrap_or("".to_string()), + ) .finish() } } -#[cfg(feature = "serde")] -impl TryFrom> for CoefficientCommitment -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ElementSerialization) -> Result { - Self::deserialize(value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ElementSerialization -where - C: Ciphersuite, -{ - fn from(value: CoefficientCommitment) -> Self { - Self(value.serialize()) - } -} - /// Contains the commitments to the coefficients for our secret polynomial _f_, /// used to generate participants' key shares. /// @@ -383,31 +321,58 @@ where Self(coefficients) } - /// Returns serialized coefficent commitments - pub fn serialize(&self) -> Vec<::Serialization> { + /// Returns serialized coefficient commitments + pub fn serialize(&self) -> Result>, Error> { self.0 .iter() - .map(|cc| <::Group as Group>::serialize(&cc.0)) - .collect() + .map(|cc| cc.serialize()) + .collect::>>() } - /// Returns VerifiableSecretSharingCommitment from a vector of serialized CoefficientCommitments - pub fn deserialize( - serialized_coefficient_commitments: Vec<::Serialization>, - ) -> Result> { + /// Serialize the whole commitment vector as a single byte vector. + pub fn serialize_whole(&self) -> Result, Error> { + self.serialize().map(|v| v.concat()) + } + + /// Returns VerifiableSecretSharingCommitment from an iterator of serialized + /// CoefficientCommitments (e.g. a [`Vec>`]). + pub fn deserialize(serialized_coefficient_commitments: I) -> Result> + where + I: IntoIterator, + V: AsRef<[u8]>, + { let mut coefficient_commitments = Vec::new(); - for cc in serialized_coefficient_commitments { - coefficient_commitments.push(CoefficientCommitment::::deserialize(cc)?); + for cc in serialized_coefficient_commitments.into_iter() { + coefficient_commitments.push(CoefficientCommitment::::deserialize(cc.as_ref())?); } Ok(Self::new(coefficient_commitments)) } + /// Deserialize a whole commitment vector from a single byte vector as returned by + /// [`VerifiableSecretSharingCommitment::serialize_whole()`]. + pub fn deserialize_whole(bytes: &[u8]) -> Result> { + // Get size from the size of the generator + let generator = ::generator(); + let len = ::serialize(&generator) + .expect("serializing the generator always works") + .as_ref() + .len(); + + let serialized_coefficient_commitments = bytes.chunks_exact(len); + + if !serialized_coefficient_commitments.remainder().is_empty() { + return Err(Error::InvalidCoefficient); + } + + Self::deserialize(serialized_coefficient_commitments) + } + /// Get the VerifyingKey matching this commitment vector (which is the first /// element in the vector), or an error if the vector is empty. pub(crate) fn verifying_key(&self) -> Result, Error> { Ok(VerifyingKey::new( - self.0.first().ok_or(Error::MissingCommitment)?.0, + self.0.first().ok_or(Error::MissingCommitment)?.0 .0, )) } @@ -416,6 +381,11 @@ where pub(crate) fn coefficients(&self) -> &[CoefficientCommitment] { &self.0 } + + /// Return the threshold associated with this commitment. + pub(crate) fn min_signers(&self) -> u16 { + self.0.len() as u16 + } } /// A secret share generated by performing a (t-out-of-n) secret sharing scheme, @@ -429,7 +399,7 @@ where /// /// To derive a FROST keypair, the receiver of the [`SecretShare`] *must* call /// .into(), which under the hood also performs validation. -#[derive(Clone, Debug, Zeroize, PartialEq, Eq, Getters)] +#[derive(Clone, Debug, Zeroize, PartialEq, Eq, Getters, ZeroizeOnDrop)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] @@ -478,16 +448,30 @@ where /// This also implements `derive_group_info()` from the [spec] (which is very similar), /// but only for this participant. /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#appendix-C.2-4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#appendix-C.2-3 pub fn verify(&self) -> Result<(VerifyingShare, VerifyingKey), Error> { - let f_result = ::generator() * self.signing_share.0; + let f_result = ::generator() * self.signing_share.to_scalar(); let result = evaluate_vss(self.identifier, &self.commitment); if !(f_result == result) { - return Err(Error::InvalidSecretShare); + // The culprit needs to be identified by the caller if needed, + // because this function is called in two different contexts: + // - after trusted dealer key generation, by the participant who + // receives the SecretShare. In that case it does not make sense + // to identify themselves as the culprit, since the issue was with + // the Coordinator or in the communication. + // - during DKG, where a "fake" SecretShare is built just to reuse + // the verification logic and it does make sense to identify the + // culprit. Note that in this case, self.identifier is the caller's + // identifier and not the culprit's, so we couldn't identify + // the culprit inside this function anyway. + return Err(Error::InvalidSecretShare { culprit: None }); } - Ok((VerifyingShare(result), self.commitment.verifying_key()?)) + Ok(( + VerifyingShare::new(result), + self.commitment.verifying_key()?, + )) } } @@ -525,18 +509,14 @@ pub enum IdentifierList<'a, C: Ciphersuite> { /// /// Implements [`trusted_dealer_keygen`] from the spec. /// -/// [`trusted_dealer_keygen`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#appendix-C +/// [`trusted_dealer_keygen`]: https://datatracker.ietf.org/doc/html/rfc9591#appendix-C pub fn generate_with_dealer( max_signers: u16, min_signers: u16, identifiers: IdentifierList, rng: &mut R, ) -> Result<(BTreeMap, SecretShare>, PublicKeyPackage), Error> { - let mut bytes = [0; 64]; - rng.fill_bytes(&mut bytes); - let key = SigningKey::new(rng); - split(&key, max_signers, min_signers, identifiers, rng) } @@ -575,7 +555,6 @@ pub fn split( } }; let mut verifying_shares: BTreeMap, VerifyingShare> = BTreeMap::new(); - let mut secret_shares_by_id: BTreeMap, SecretShare> = BTreeMap::new(); for secret_share in secret_shares { @@ -585,14 +564,18 @@ pub fn split( secret_shares_by_id.insert(secret_share.identifier, secret_share); } - Ok(( - secret_shares_by_id, - PublicKeyPackage { - header: Header::default(), - verifying_shares, - verifying_key, - }, - )) + let public_key_package = PublicKeyPackage { + header: Header::default(), + verifying_shares, + verifying_key, + min_signers: Some(min_signers), + }; + + // Apply post-processing + let (processed_secret_shares, processed_public_key_package) = + C::post_generate(secret_shares_by_id, public_key_package)?; + + Ok((processed_secret_shares, processed_public_key_package)) } /// Evaluate the polynomial with the given coefficients (constant term first) @@ -600,17 +583,17 @@ pub fn split( /// /// Implements [`polynomial_evaluate`] from the spec. /// -/// [`polynomial_evaluate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-evaluation-of-a-polynomial +/// [`polynomial_evaluate`]: https://datatracker.ietf.org/doc/html/rfc9591#name-additional-polynomial-opera fn evaluate_polynomial( identifier: Identifier, coefficients: &[Scalar], ) -> Scalar { let mut value = <::Field>::zero(); - let ell_scalar = identifier; + let ell = identifier; for coeff in coefficients.iter().skip(1).rev() { value = value + *coeff; - value *= ell_scalar; + value = value * ell.to_scalar(); } value = value + *coefficients @@ -628,11 +611,13 @@ fn evaluate_vss( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, ) -> Element { - let i = identifier; + let i = identifier.to_scalar(); let (_, result) = commitment.0.iter().fold( (<::Field>::one(), ::identity()), - |(i_to_the_k, sum_so_far), comm_k| (i * i_to_the_k, sum_so_far + comm_k.0 * i_to_the_k), + |(i_to_the_k, sum_so_far), comm_k| { + (i * i_to_the_k, sum_so_far + comm_k.value() * i_to_the_k) + }, ); result } @@ -643,7 +628,7 @@ fn evaluate_vss( /// When using a central dealer, [`SecretShare`]s are distributed to /// participants, who then perform verification, before deriving /// [`KeyPackage`]s, which they store to later use during signing. -#[derive(Clone, Debug, PartialEq, Eq, Getters, Zeroize)] +#[derive(Clone, Debug, PartialEq, Eq, Getters, Zeroize, ZeroizeOnDrop)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] @@ -727,7 +712,7 @@ where signing_share: secret_share.signing_share, verifying_share, verifying_key, - min_signers: secret_share.commitment.0.len() as u16, + min_signers: secret_share.commitment.min_signers(), }) } } @@ -737,7 +722,7 @@ where /// /// Used for verification purposes before publishing a signature. #[derive(Clone, Debug, PartialEq, Eq, Getters)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct PublicKeyPackage { @@ -749,6 +734,12 @@ pub struct PublicKeyPackage { pub(crate) verifying_shares: BTreeMap, VerifyingShare>, /// The joint public key for the entire group. pub(crate) verifying_key: VerifyingKey, + /// The minimum number of signers (threshold) required for the group. + /// This can be None in packages created with `frost_core` prior to 3.0.0. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "serde", serde(default))] + #[getter(copy)] + pub(crate) min_signers: Option, } impl PublicKeyPackage @@ -756,14 +747,32 @@ where C: Ciphersuite, { /// Create a new [`PublicKeyPackage`] instance. + /// + /// `min_signers` is an `Option` for compatibility with pre-3.0.0 packages. + /// In normal usage, it should always be `Some(value)`. pub fn new( verifying_shares: BTreeMap, VerifyingShare>, verifying_key: VerifyingKey, + min_signers: Option, + ) -> Self { + Self::new_internal(verifying_shares, verifying_key, min_signers) + } + + /// Create a new [`PublicKeyPackage`] instance, allowing not specifying the + /// number of signers. This is used internally, in particular for testing + /// old [`PublicKeyPackage`]s that do not encode the `min_signers`. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new_internal( + verifying_shares: BTreeMap, VerifyingShare>, + verifying_key: VerifyingKey, + min_signers: Option, ) -> Self { Self { header: Header::default(), verifying_shares, verifying_key, + min_signers, } } @@ -782,6 +791,7 @@ where Ok(PublicKeyPackage::new( verifying_keys, VerifyingKey::from_commitment(commitment)?, + Some(commitment.min_signers()), )) } @@ -798,6 +808,11 @@ where let group_commitment = sum_commitments(&commitments)?; Self::from_commitment(&identifiers, &group_commitment) } + + /// Return the maximum number of signers. + pub fn max_signers(&self) -> u16 { + self.verifying_shares.len() as u16 + } } #[cfg(feature = "serialization")] @@ -816,6 +831,8 @@ where } } +/// Validates the number of signers. +#[cfg_attr(feature = "internals", visibility::make(pub))] fn validate_num_of_signers( min_signers: u16, max_signers: u16, @@ -861,7 +878,7 @@ pub(crate) fn generate_secret_polynomial( // Create the vector of commitments let commitment: Vec<_> = coefficients .iter() - .map(|c| CoefficientCommitment(::generator() * *c)) + .map(|c| CoefficientCommitment::new(::generator() * *c)) .collect(); let commitment: VerifiableSecretSharingCommitment = VerifiableSecretSharingCommitment(commitment); @@ -887,7 +904,7 @@ pub(crate) fn generate_secret_polynomial( /// /// Implements [`secret_share_shard`] from the spec. /// -/// [`secret_share_shard`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#appendix-C.1 +/// [`secret_share_shard`]: https://datatracker.ietf.org/doc/html/rfc9591#name-shamir-secret-sharing pub(crate) fn generate_secret_shares( secret: &SigningKey, max_signers: u16, @@ -900,7 +917,7 @@ pub(crate) fn generate_secret_shares( let (coefficients, commitment) = generate_secret_polynomial(secret, max_signers, min_signers, coefficients)?; - let identifiers_set: HashSet<_> = identifiers.iter().collect(); + let identifiers_set: BTreeSet<_> = identifiers.iter().collect(); if identifiers_set.len() != identifiers.len() { return Err(Error::DuplicatedIdentifier); } @@ -968,7 +985,7 @@ pub fn reconstruct( compute_lagrange_coefficient(&identifiers, None, key_package.identifier)?; // Compute y = f(0) via polynomial interpolation of these t-of-n solutions ('points) of f - secret = secret + (lagrange_coefficient * key_package.signing_share().0); + secret = secret + (lagrange_coefficient * key_package.signing_share().to_scalar()); } Ok(SigningKey { scalar: secret }) diff --git a/frost-core/src/keys/dkg.rs b/frost-core/src/keys/dkg.rs index fa3706518..0c4d77d6a 100644 --- a/frost-core/src/keys/dkg.rs +++ b/frost-core/src/keys/dkg.rs @@ -2,7 +2,11 @@ //! //! The DKG module supports generating FROST key shares in a distributed manner, //! without a trusted dealer, via two rounds of communication between all -//! participants. +//! participants (with one round requiring a broadcast channel, so totalling +//! three rounds of communication if using echo broadcast). +//! +//! For a higher level tutorial on how to use it, refer to the [ZF FROST +//! Book](https://frost.zfnd.org/tutorial/dkg.html). //! //! This implements FROST KeyGen from the original [FROST paper], specifically //! Figure 1. This protocol is a variant of [Pedersen's DKG] that additionally @@ -17,7 +21,7 @@ //! //! As required for any multi-party protocol using Feldman's VSS, the key //! generation stage in FROST requires participants to maintain a consistent -//! view of the pubic commitments to the secret polynomial coefficients. This +//! view of the public commitments to the secret polynomial coefficients. This //! DKG protocol requires participants to broadcast the commitment values //! honestly (e.g., participants do not provide different commitment values to a //! subset of participants) over a _[secure broadcast channel]_. @@ -30,7 +34,9 @@ //! [Feldman's VSS]: https://www.cs.umd.edu/~gasarch/TOPICS/secretsharing/feldmanVSS.pdf //! [secure broadcast channel]: https://frost.zfnd.org/terminology.html#broadcast-channel -use std::{collections::BTreeMap, iter}; +use core::iter; + +use alloc::collections::BTreeMap; use rand_core::{CryptoRng, RngCore}; @@ -39,6 +45,9 @@ use crate::{ SigningKey, VerifyingKey, }; +#[cfg(feature = "serialization")] +use crate::serialization::{Deserialize, Serialize}; + use super::{ evaluate_polynomial, generate_coefficients, generate_secret_polynomial, validate_num_of_signers, KeyPackage, PublicKeyPackage, SecretShare, SigningShare, @@ -47,13 +56,16 @@ use super::{ /// DKG Round 1 structures. pub mod round1 { + use alloc::vec::Vec; use derive_getters::Getters; - use zeroize::Zeroize; - - use crate::serialization::{Deserialize, Serialize}; + use zeroize::{Zeroize, ZeroizeOnDrop}; use super::*; + use crate::serialization::SerializableScalar; + #[cfg(feature = "serialization")] + use crate::serialization::{Deserialize, Serialize}; + /// The package that must be broadcast by each participant to all other participants /// between the first and second parts of the DKG protocol (round 1). #[derive(Clone, Debug, PartialEq, Eq, Getters)] @@ -109,14 +121,20 @@ pub mod round1 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq, Getters, Zeroize, ZeroizeOnDrop)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] + #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct SecretPackage { /// The identifier of the participant holding the secret. + #[zeroize(skip)] pub(crate) identifier: Identifier, /// Coefficients of the temporary secret polynomial for the participant. /// These are (a_{i0}, ..., a_{i(t−1)})) which define the polynomial f_i(x) - pub(crate) coefficients: Vec>, + #[getter(skip)] + pub(crate) coefficients: Vec>, /// The public commitment for the participant (C_i) + #[zeroize(skip)] pub(crate) commitment: VerifiableSecretSharingCommitment, /// The minimum number of signers. pub(crate) min_signers: u16, @@ -128,18 +146,53 @@ pub mod round1 { where C: Ciphersuite, { + /// Create a new Secret Package. This should be called only if + /// custom serialization is required; run the DKG to create + /// a valid SecretPackage. + pub fn new( + identifier: Identifier, + coefficients: Vec>, + commitment: VerifiableSecretSharingCommitment, + min_signers: u16, + max_signers: u16, + ) -> Self { + Self { + identifier, + coefficients: coefficients.into_iter().map(SerializableScalar).collect(), + commitment, + min_signers, + max_signers, + } + } + /// Returns the secret coefficients. - #[cfg(feature = "internals")] - pub fn coefficients(&self) -> &[Scalar] { - &self.coefficients + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn coefficients(&self) -> Vec> { + self.coefficients.iter().map(|s| s.0).collect() } } - impl std::fmt::Debug for SecretPackage + #[cfg(feature = "serialization")] + impl SecretPackage where C: Ciphersuite, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } + } + + impl core::fmt::Debug for SecretPackage + where + C: Ciphersuite, + { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SecretPackage") .field("identifier", &self.identifier) .field("coefficients", &"") @@ -149,25 +202,17 @@ pub mod round1 { .finish() } } - - impl Zeroize for SecretPackage - where - C: Ciphersuite, - { - fn zeroize(&mut self) { - for c in self.coefficients.iter_mut() { - *c = <::Field>::zero(); - } - } - } } /// DKG Round 2 structures. pub mod round2 { use derive_getters::Getters; - use zeroize::Zeroize; + use zeroize::{Zeroize, ZeroizeOnDrop}; - use crate::serialization::{Deserialize, Serialize}; + #[cfg(feature = "serialization")] + use alloc::vec::Vec; + + use crate::serialization::SerializableScalar; use super::*; @@ -225,25 +270,75 @@ pub mod round2 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq, Getters, Zeroize, ZeroizeOnDrop)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] + #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct SecretPackage { /// The identifier of the participant holding the secret. + #[zeroize(skip)] pub(crate) identifier: Identifier, /// The public commitment from the participant (C_i) + #[zeroize(skip)] pub(crate) commitment: VerifiableSecretSharingCommitment, /// The participant's own secret share (f_i(i)). - pub(crate) secret_share: Scalar, + #[getter(skip)] + pub(crate) secret_share: SerializableScalar, /// The minimum number of signers. pub(crate) min_signers: u16, /// The total number of signers. pub(crate) max_signers: u16, } - impl std::fmt::Debug for SecretPackage + impl SecretPackage + where + C: Ciphersuite, + { + /// Create a new SecretPackage. Use this only if custom serialization is + /// required; run the DKG to create a valid SecretPackage. + pub fn new( + identifier: Identifier, + commitment: VerifiableSecretSharingCommitment, + secret_share: Scalar, + min_signers: u16, + max_signers: u16, + ) -> Self { + Self { + identifier, + commitment, + secret_share: SerializableScalar(secret_share), + min_signers, + max_signers, + } + } + + /// Return the SecretShare. + pub fn secret_share(&self) -> Scalar { + self.secret_share.0 + } + } + + #[cfg(feature = "serialization")] + impl SecretPackage where C: Ciphersuite, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } + } + + impl core::fmt::Debug for SecretPackage + where + C: Ciphersuite, + { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SecretPackage") .field("identifier", &self.identifier) .field("commitment", &self.commitment) @@ -253,15 +348,6 @@ pub mod round2 { .finish() } } - - impl Zeroize for SecretPackage - where - C: Ciphersuite, - { - fn zeroize(&mut self) { - self.secret_share = <::Field>::zero(); - } - } } /// Performs the first part of the distributed key generation protocol @@ -269,7 +355,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -295,13 +381,13 @@ pub fn part1( let proof_of_knowledge = compute_proof_of_knowledge(identifier, &coefficients, &commitment, &mut rng)?; - let secret_package = round1::SecretPackage { + let secret_package = round1::SecretPackage::new( identifier, coefficients, - commitment: commitment.clone(), + commitment.clone(), min_signers, max_signers, - }; + ); let package = round1::Package { header: Header::default(), commitment, @@ -316,17 +402,19 @@ fn challenge( identifier: Identifier, verifying_key: &VerifyingKey, R: &Element, -) -> Option> +) -> Result, Error> where C: Ciphersuite, { let mut preimage = vec![]; preimage.extend_from_slice(identifier.serialize().as_ref()); - preimage.extend_from_slice(::serialize(&verifying_key.element).as_ref()); - preimage.extend_from_slice(::serialize(R).as_ref()); + preimage.extend_from_slice(::serialize(&verifying_key.to_element())?.as_ref()); + preimage.extend_from_slice(::serialize(R)?.as_ref()); - Some(Challenge(C::HDKG(&preimage[..])?)) + Ok(Challenge( + C::HDKG(&preimage[..]).ok_or(Error::DKGNotSupported)?, + )) } /// Compute the proof of knowledge of the secret coefficients used to generate @@ -344,13 +432,9 @@ pub(crate) fn compute_proof_of_knowledge // > a_{i0} by calculating σ_i = (R_i, μ_i), such that k ← Z_q, R_i = g^k, // > c_i = H(i, Φ, g^{a_{i0}} , R_i), μ_i = k + a_{i0} · c_i, with Φ being // > a context string to prevent replay attacks. - let k = <::Field>::random(&mut rng); - let R_i = ::generator() * k; - let c_i = challenge::(identifier, &commitment.verifying_key()?, &R_i) - .ok_or(Error::DKGNotSupported)?; - let a_i0 = *coefficients - .first() - .expect("coefficients must have at least one element"); + let (k, R_i) = ::generate_nonce(&mut rng); + let c_i = challenge::(identifier, &commitment.verifying_key()?, &R_i)?; + let a_i0 = *coefficients.first().ok_or(Error::InvalidCoefficients)?; let mu_i = k + a_i0 * c_i.0; Ok(Signature { R: R_i, z: mu_i }) } @@ -361,7 +445,7 @@ pub(crate) fn compute_proof_of_knowledge pub(crate) fn verify_proof_of_knowledge( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, - proof_of_knowledge: Signature, + proof_of_knowledge: &Signature, ) -> Result<(), Error> { // Round 1, Step 5 // @@ -372,26 +456,28 @@ pub(crate) fn verify_proof_of_knowledge( let R_ell = proof_of_knowledge.R; let mu_ell = proof_of_knowledge.z; let phi_ell0 = commitment.verifying_key()?; - let c_ell = challenge::(ell, &phi_ell0, &R_ell).ok_or(Error::DKGNotSupported)?; - if R_ell != ::generator() * mu_ell - phi_ell0.element * c_ell.0 { + let c_ell = challenge::(ell, &phi_ell0, &R_ell)?; + if R_ell != ::generator() * mu_ell - phi_ell0.to_element() * c_ell.0 { return Err(Error::InvalidProofOfKnowledge { culprit: ell }); } Ok(()) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// `round1_packages` maps the identifier of each participant to the -/// [`round1::Package`] they sent. These identifiers must come from whatever mapping -/// the coordinator has between communication channels and participants, i.e. -/// they must have assurance that the [`round1::Package`] came from -/// the participant with that identifier. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// participant has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the a map of [`round2::Package`]s that -/// must be sent to each participant who has the given identifier in the map key. +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, round1::Package>, @@ -406,8 +492,12 @@ pub fn part2( return Err(Error::IncorrectNumberOfPackages); } + if round1_packages.contains_key(&secret_package.identifier) { + return Err(Error::UnknownIdentifier); + } + for package in round1_packages.values() { - if package.commitment.0.len() != secret_package.min_signers as usize { + if package.commitment.min_signers() != secret_package.min_signers { return Err(Error::IncorrectNumberOfCommitments); } } @@ -420,7 +510,7 @@ pub fn part2( verify_proof_of_knowledge( ell, &round1_package.commitment, - round1_package.proof_of_knowledge, + &round1_package.proof_of_knowledge, )?; // Round 2, Step 1 @@ -428,7 +518,7 @@ pub fn part2( // > Each P_i securely sends to each other participant P_ℓ a secret share (ℓ, f_i(ℓ)), // > deleting f_i and each share afterward except for (i, f_i(i)), // > which they keep for themselves. - let signing_share = SigningShare::from_coefficients(&secret_package.coefficients, ell); + let signing_share = SigningShare::from_coefficients(&secret_package.coefficients(), ell); round2_packages.insert( ell, @@ -438,36 +528,36 @@ pub fn part2( }, ); } - let fii = evaluate_polynomial(secret_package.identifier, &secret_package.coefficients); + let fii = evaluate_polynomial(secret_package.identifier, &secret_package.coefficients()); Ok(( - round2::SecretPackage { - identifier: secret_package.identifier, - commitment: secret_package.commitment, - secret_share: fii, - min_signers: secret_package.min_signers, - max_signers: secret_package.max_signers, - }, + round2::SecretPackage::new( + secret_package.identifier, + secret_package.commitment.clone(), + fii, + secret_package.min_signers, + secret_package.max_signers, + ), round2_packages, )) } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. /// /// `round1_packages` must be the same used in [`part2()`]. /// -/// `round2_packages` maps the identifier of each participant to the -/// [`round2::Package`] they sent. These identifiers must come from whatever mapping -/// the coordinator has between communication channels and participants, i.e. -/// they must have assurance that the [`round2::Package`] came from -/// the participant with that identifier. +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// participant has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, round1::Package>, @@ -476,6 +566,12 @@ pub fn part3( if round1_packages.len() != (round2_secret_package.max_signers - 1) as usize { return Err(Error::IncorrectNumberOfPackages); } + if round1_packages.contains_key(&round2_secret_package.identifier) { + return Err(Error::UnknownIdentifier); + } + if round2_packages.contains_key(&round2_secret_package.identifier) { + return Err(Error::UnknownIdentifier); + } if round1_packages.len() != round2_packages.len() { return Err(Error::IncorrectNumberOfPackages); } @@ -513,17 +609,26 @@ pub fn part3( }; // Verify the share. We don't need the result. - let _ = secret_share.verify()?; + // Identify the culprit if an InvalidSecretShare error is returned. + let _ = secret_share.verify().map_err(|e| { + if let Error::InvalidSecretShare { .. } = e { + Error::InvalidSecretShare { + culprit: Some(*sender_identifier), + } + } else { + e + } + })?; // Round 2, Step 3 // // > Each P_i calculates their long-lived private signing share by computing // > s_i = ∑^n_{ℓ=1} f_ℓ(i), stores s_i securely, and deletes each f_ℓ(i). - signing_share = signing_share + f_ell_i.0; + signing_share = signing_share + f_ell_i.to_scalar(); } - signing_share = signing_share + round2_secret_package.secret_share; - let signing_share = SigningShare(signing_share); + signing_share = signing_share + round2_secret_package.secret_share(); + let signing_share = SigningShare::new(signing_share); // Round 2, Step 4 // @@ -549,5 +654,5 @@ pub fn part3( min_signers: round2_secret_package.min_signers, }; - Ok((key_package, public_key_package)) + C::post_dkg(key_package, public_key_package) } diff --git a/frost-core/src/keys/refresh.rs b/frost-core/src/keys/refresh.rs new file mode 100644 index 000000000..cd80ad042 --- /dev/null +++ b/frost-core/src/keys/refresh.rs @@ -0,0 +1,484 @@ +//! Refresh Shares +//! +//! Refreshing shares has two purposes: +//! +//! - Mitigate against share compromise. +//! - Remove participants from a group. +//! +//! Refer to the [FROST +//! book](https://frost.zfnd.org/frost.html#refreshing-shares) for important +//! details. +//! +//! This modules supports refreshing shares using a Trusted Dealer or DKG. You +//! probably want to use the same approach as the original share generation. +//! +//! For the Trusted Dealer approach, the trusted dealer should call +//! [`compute_refreshing_shares()`] and send the returned refreshing shares to +//! the participants. Each participant should then call [`refresh_share()`]. +//! +//! For the DKG approach, the flow is very similar to [DKG +//! itself](`https://frost.zfnd.org/tutorial/dkg.html`). Each participant calls +//! [`refresh_dkg_part1()`], keeps the returned secret package and sends the +//! returned package to other participants. Then each participants calls +//! [`refresh_dkg_part2()`] and sends the returned packages to the other +//! participants. Finally each participant calls [`refresh_dkg_shares()`]. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use crate::{ + keys::dkg::{compute_proof_of_knowledge, round1, round2}, + keys::{ + evaluate_polynomial, generate_coefficients, generate_secret_polynomial, + generate_secret_shares, validate_num_of_signers, CoefficientCommitment, PublicKeyPackage, + SigningKey, SigningShare, VerifyingShare, + }, + Ciphersuite, CryptoRng, Error, Field, Group, Header, Identifier, RngCore, +}; + +use core::iter; + +use super::{dkg::round1::Package, KeyPackage, SecretShare, VerifiableSecretSharingCommitment}; + +/// Compute refreshing shares for the Trusted Dealer refresh procedure. +/// +/// - `pub_key_package`: the current public key package. Note: if a pre-3.0.0 +/// generate package is used, you will need to manually set the `min_signers` +/// field with the theshold that was used in the original share generation. +/// (You can't change the threshold when refreshing shares.) +/// - `identifiers`: The identifiers of all participants that want to refresh +/// their shares. Must be a subset of the identifiers in `pub_key_package`. If +/// not all identifiers are passed, the refresh procedure will effectively +/// remove the missing participants. The length must be equal to or greater +/// than the threshold of the group. +/// +/// It returns a vectors of [`SecretShare`] that must be sent to the +/// participants in the same order as `identifiers`, and the refreshed +/// [`PublicKeyPackage`]. +pub fn compute_refreshing_shares( + pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + rng: &mut R, +) -> Result<(Vec>, PublicKeyPackage), Error> { + let min_signers = pub_key_package + .min_signers + .ok_or(Error::InvalidMinSigners)?; + + let signers = identifiers.len() as u16; + validate_num_of_signers(min_signers, signers)?; + + if identifiers + .iter() + .any(|i| !pub_key_package.verifying_shares().contains_key(i)) + { + return Err(Error::UnknownIdentifier); + } + + // Build refreshing shares + let refreshing_key = SigningKey { + scalar: <::Field>::zero(), + }; + + let coefficients = generate_coefficients::(min_signers as usize - 1, rng); + let refreshing_shares = generate_secret_shares( + &refreshing_key, + signers, + min_signers, + coefficients, + identifiers, + )?; + + let mut refreshed_verifying_shares: BTreeMap, VerifyingShare> = + BTreeMap::new(); + let mut refreshing_shares_minus_identity: Vec> = Vec::new(); + + for mut share in refreshing_shares { + let refreshing_verifying_share: VerifyingShare = SigningShare::into(share.signing_share); + + let verifying_share = pub_key_package.verifying_shares.get(&share.identifier); + + match verifying_share { + Some(verifying_share) => { + let refreshed_verifying_share = + refreshing_verifying_share.to_element() + verifying_share.to_element(); + refreshed_verifying_shares.insert( + share.identifier, + VerifyingShare::new(refreshed_verifying_share), + ); + } + None => return Err(Error::UnknownIdentifier), + }; + + share.commitment.0.remove(0); + refreshing_shares_minus_identity.push(share); + } + + let refreshed_pub_key_package = PublicKeyPackage:: { + header: pub_key_package.header, + verifying_shares: refreshed_verifying_shares, + verifying_key: pub_key_package.verifying_key, + min_signers: Some(pub_key_package.min_signers.unwrap_or(min_signers)), + }; + + Ok((refreshing_shares_minus_identity, refreshed_pub_key_package)) +} + +/// Refresh a share in the Trusted Dealer refresh procedure. +/// +/// Must be called by each participant refreshing the shares, with the +/// `refreshing_share` received from the trusted dealer and the +/// `current_key_package` of the participant. +pub fn refresh_share( + mut refreshing_share: SecretShare, + current_key_package: &KeyPackage, +) -> Result, Error> { + // The identity commitment needs to be added to the VSS commitment + let identity_commitment: Vec> = + vec![CoefficientCommitment::new(C::Group::identity())]; + + let refreshing_share_commitments: Vec> = identity_commitment + .into_iter() + .chain(refreshing_share.commitment.0.clone()) + .collect(); + + refreshing_share.commitment = + VerifiableSecretSharingCommitment::::new(refreshing_share_commitments); + + // Verify refreshing_share secret share + let refreshed_share_package = KeyPackage::::try_from(refreshing_share)?; + + if refreshed_share_package.min_signers() != current_key_package.min_signers() { + return Err(Error::InvalidMinSigners); + } + + let signing_share: SigningShare = SigningShare::new( + refreshed_share_package.signing_share.to_scalar() + + current_key_package.signing_share.to_scalar(), + ); + + let mut new_key_package = current_key_package.clone(); + new_key_package.signing_share = signing_share; + + Ok(new_key_package) +} + +/// Part 1 of refresh share with DKG. +/// +/// - `identifier`: The identifier of the participant that wants to refresh +/// their share. +/// - `max_signers`: the number of participants that are refreshing their +/// shares. It can be smaller than the original value, but still equal to or +/// greater than `min_signers`. +/// - `min_signers`: the threshold needed to sign. It must be equal to the +/// original value for the group (i.e. the refresh process can't reduce +/// the threshold). +/// +/// It returns the [`round1::SecretPackage`] that must be kept in memory +/// by the participant for the other steps, and the [`round1::Package`] that +/// must be sent to each other participant in the refresh run. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + validate_num_of_signers::(min_signers, max_signers)?; + + // Build refreshing shares + let refreshing_key = SigningKey { + scalar: <::Field>::zero(), + }; + + // Round 1, Step 1 + let coefficients = generate_coefficients::(min_signers as usize - 1, &mut rng); + + let (coefficients, commitment) = + generate_secret_polynomial(&refreshing_key, max_signers, min_signers, coefficients)?; + + // Remove identity element from coefficients + let mut coeff_comms = commitment.0; + coeff_comms.remove(0); + let commitment = VerifiableSecretSharingCommitment::new(coeff_comms.clone()); + + let proof_of_knowledge = + compute_proof_of_knowledge(identifier, &coefficients, &commitment, &mut rng)?; + + let secret_package = round1::SecretPackage::new( + identifier, + coefficients.clone(), + commitment.clone(), + min_signers, + max_signers, + ); + let package = round1::Package { + header: Header::default(), + commitment, + proof_of_knowledge, + }; + + Ok((secret_package, package)) +} + +/// Performs the second part of the refresh procedure for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. +/// +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// participant has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. +pub fn refresh_dkg_part2( + mut secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, round1::Package>, +) -> Result< + ( + round2::SecretPackage, + BTreeMap, round2::Package>, + ), + Error, +> { + if round1_packages.len() != (secret_package.max_signers - 1) as usize { + return Err(Error::IncorrectNumberOfPackages); + } + + // The identity commitment needs to be added to the VSS commitment for secret package + let identity_commitment: Vec> = + vec![CoefficientCommitment::new(C::Group::identity())]; + + let refreshing_secret_share_commitments: Vec> = identity_commitment + .into_iter() + .chain(secret_package.commitment.0.clone()) + .collect(); + + secret_package.commitment = + VerifiableSecretSharingCommitment::::new(refreshing_secret_share_commitments); + + let mut round2_packages = BTreeMap::new(); + + for (sender_identifier, round1_package) in round1_packages { + // The identity commitment needs to be added to the VSS commitment for every round 1 package + let identity_commitment: Vec> = + vec![CoefficientCommitment::new(C::Group::identity())]; + + let refreshing_share_commitments: Vec> = identity_commitment + .into_iter() + .chain(round1_package.commitment.0.clone()) + .collect(); + + if refreshing_share_commitments.clone().len() != secret_package.min_signers as usize { + return Err(Error::IncorrectNumberOfCommitments); + } + + let ell = *sender_identifier; + + // Round 1, Step 5 + // We don't need to verify the proof of knowledge + + // Round 2, Step 1 + // + // > Each P_i securely sends to each other participant P_ℓ a secret share (ℓ, f_i(ℓ)), + // > deleting f_i and each share afterward except for (i, f_i(i)), + // > which they keep for themselves. + let signing_share = SigningShare::from_coefficients(&secret_package.coefficients(), ell); + + round2_packages.insert( + ell, + round2::Package { + header: Header::default(), + signing_share, + }, + ); + } + let fii = evaluate_polynomial(secret_package.identifier, &secret_package.coefficients()); + + // We remove the identity again to make it serializable + secret_package.commitment.0.remove(0); + Ok(( + round2::SecretPackage::new( + secret_package.identifier, + secret_package.commitment.clone(), + fii, + secret_package.min_signers, + secret_package.max_signers, + ), + round2_packages, + )) +} + +/// Performs the third and final part of the refresh procedure for the +/// participant holding the given [`round2::SecretPackage`], given the received +/// [`round1::Package`]s and [`round2::Package`]s received from the other +/// participants. +/// +/// `round1_packages` must be the same used in [`refresh_dkg_part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// participant has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. +/// +/// `old_pub_key_package` and `old_key_package` are the old values from the +/// participant, which are being refreshed. +/// +/// It returns the refreshed [`KeyPackage`] that has the long-lived key share +/// for the participant, and the refreshed [`PublicKeyPackage`]s that has public +/// information about all participants; both of which are required to compute +/// FROST signatures. Note that while the verifying (group) key of the +/// [`PublicKeyPackage`] will stay the same, the verifying shares will change. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, round1::Package>, + round2_packages: &BTreeMap, round2::Package>, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + if round2_secret_package.min_signers() != old_key_package.min_signers() { + return Err(Error::InvalidMinSigners); + } + + // Add identity commitment back into the round2_secret_package + let mut commitment = round2_secret_package.commitment.0.clone(); + commitment.insert(0, CoefficientCommitment::new(C::Group::identity())); + let round2_secret_package = round2::SecretPackage::new( + round2_secret_package.identifier, + VerifiableSecretSharingCommitment::::new(commitment), + round2_secret_package.secret_share.0, + round2_secret_package.min_signers, + round2_secret_package.max_signers, + ); + + // Add identity commitment back into round1_packages + let mut new_round_1_packages = BTreeMap::new(); + for (sender_identifier, round1_package) in round1_packages { + // The identity commitment needs to be added to the VSS commitment for every round 1 package + let identity_commitment: Vec> = + vec![CoefficientCommitment::new(C::Group::identity())]; + + let refreshing_share_commitments: Vec> = identity_commitment + .into_iter() + .chain(round1_package.commitment.0.clone()) + .collect(); + + let new_commitments = + VerifiableSecretSharingCommitment::::new(refreshing_share_commitments); + + let new_round_1_package = Package { + header: round1_package.header, + commitment: new_commitments, + proof_of_knowledge: round1_package.proof_of_knowledge, + }; + + new_round_1_packages.insert(*sender_identifier, new_round_1_package); + } + + if new_round_1_packages.len() != (round2_secret_package.max_signers - 1) as usize { + return Err(Error::IncorrectNumberOfPackages); + } + if new_round_1_packages.len() != round2_packages.len() { + return Err(Error::IncorrectNumberOfPackages); + } + if new_round_1_packages + .keys() + .any(|id| !round2_packages.contains_key(id)) + { + return Err(Error::IncorrectPackage); + } + + let mut signing_share = <::Field>::zero(); + + for (sender_identifier, round2_package) in round2_packages { + // Round 2, Step 2 + // + // > Each P_i verifies their shares by calculating: + // > g^{f_ℓ(i)} ≟ ∏^{t−1}_{k=0} φ^{i^k mod q}_{ℓk}, aborting if the + // > check fails. + let ell = *sender_identifier; + let f_ell_i = round2_package.signing_share; + + let commitment = &new_round_1_packages + .get(&ell) + .ok_or(Error::PackageNotFound)? + .commitment; + + // The verification is exactly the same as the regular SecretShare verification; + // however the required components are in different places. + // Build a temporary SecretShare so what we can call verify(). + let secret_share = SecretShare { + header: Header::default(), + identifier: round2_secret_package.identifier, + signing_share: f_ell_i, + commitment: commitment.clone(), + }; + + // Verify the share. We don't need the result. + let _ = secret_share.verify()?; + + // Round 2, Step 3 + // + // > Each P_i calculates their long-lived private signing share by computing + // > s_i = ∑^n_{ℓ=1} f_ℓ(i), stores s_i securely, and deletes each f_ℓ(i). + signing_share = signing_share + f_ell_i.to_scalar(); + } + + signing_share = signing_share + round2_secret_package.secret_share(); + + // Build new signing share + let old_signing_share = old_key_package.signing_share.to_scalar(); + signing_share = signing_share + old_signing_share; + let signing_share = SigningShare::new(signing_share); + + // Round 2, Step 4 + // + // > Each P_i calculates their public verification share Y_i = g^{s_i}. + let verifying_share = signing_share.into(); + + let commitments: BTreeMap<_, _> = new_round_1_packages + .iter() + .map(|(id, package)| (*id, &package.commitment)) + .chain(iter::once(( + round2_secret_package.identifier, + &round2_secret_package.commitment, + ))) + .collect(); + + let zero_shares_public_key_package = PublicKeyPackage::from_dkg_commitments(&commitments)?; + + let mut new_verifying_shares = BTreeMap::new(); + + for (identifier, verifying_share) in zero_shares_public_key_package.verifying_shares { + let new_verifying_share = verifying_share.to_element() + + old_pub_key_package + .verifying_shares + .get(&identifier) + .ok_or(Error::UnknownIdentifier)? + .to_element(); + new_verifying_shares.insert(identifier, VerifyingShare::new(new_verifying_share)); + } + + let public_key_package = PublicKeyPackage { + header: old_pub_key_package.header, + verifying_shares: new_verifying_shares, + verifying_key: old_pub_key_package.verifying_key, + min_signers: Some(round2_secret_package.min_signers), + }; + + let key_package = KeyPackage { + header: Header::default(), + identifier: round2_secret_package.identifier, + signing_share, + verifying_share, + verifying_key: public_key_package.verifying_key, + min_signers: round2_secret_package.min_signers, + }; + + Ok((key_package, public_key_package)) +} diff --git a/frost-core/src/keys/repairable.rs b/frost-core/src/keys/repairable.rs index 169e0ab83..0d6263657 100644 --- a/frost-core/src/keys/repairable.rs +++ b/frost-core/src/keys/repairable.rs @@ -1,38 +1,128 @@ //! Repairable Threshold Scheme //! -//! Implements the Repairable Threshold Scheme (RTS) from . -//! The RTS is used to help a signer (participant) repair their lost share. This is achieved -//! using a subset of the other signers know here as `helpers`. - -use std::collections::{BTreeMap, BTreeSet}; - +//! Implements the Repairable Threshold Scheme (RTS) from +//! . The RTS is used to help a signer +//! (participant) repair their lost share. This is achieved using a subset of +//! the other signers known here as `helpers`. +//! +//! The repair procedure should be run as follows: +//! +//! - Participants need to agree somehow on who are going to be the `helpers` +//! for the repair, and which participant is going to repair their share. +//! - Each helper runs `repair_share_part1`, generating a set of `delta` values +//! to be sent to each helper (including themselves). +//! - Each helper runs `repair_share_part2`, passing the received `delta` +//! values, generating a `sigma` value to be sent to the participant repairing +//! their share. +//! - The participant repairing their share runs `repair_share_part3`, passing +//! all the received `sigma` values, recovering their lost `KeyPackage`. (They +//! will also need the `PublicKeyPackage` for this step which could be +//! provided by any of the helpers). + +use alloc::collections::{BTreeMap, BTreeSet}; + +use alloc::vec::Vec; + +use crate::keys::{KeyPackage, PublicKeyPackage}; +use crate::serialization::SerializableScalar; use crate::{ - compute_lagrange_coefficient, Ciphersuite, CryptoRng, Error, Field, Group, Header, Identifier, - RngCore, Scalar, + compute_lagrange_coefficient, Ciphersuite, CryptoRng, Error, Field, Group, Identifier, RngCore, + Scalar, }; -use super::{generate_coefficients, SecretShare, SigningShare, VerifiableSecretSharingCommitment}; +use super::{generate_coefficients, SigningShare}; + +/// A delta value which is the output of part 1 of RTS. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Delta(pub(crate) SerializableScalar); + +impl Delta +where + C: Ciphersuite, +{ + /// Create a new [`Delta`] from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Self { + Self(SerializableScalar(scalar)) + } + + /// Get the inner scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 + } -/// Step 1 of RTS. + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) + } + + /// Serialize to bytes + pub fn serialize(&self) -> Vec { + self.0.serialize() + } +} + +/// A sigma value which is the output of part 2 of RTS. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Sigma(pub(crate) SerializableScalar); + +impl Sigma +where + C: Ciphersuite, +{ + /// Create a new [`Sigma`] from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Self { + Self(SerializableScalar(scalar)) + } + + /// Get the inner scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) + } + + /// Serialize to bytes + pub fn serialize(&self) -> Vec { + self.0.serialize() + } +} + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Scalar>, Error> { - if helpers.len() < 2 { - return Err(Error::InvalidMinSigners); - } - - if helpers.is_empty() { +) -> Result, Delta>, Error> { + if helpers.len() < *key_package_i.min_signers() as usize { return Err(Error::IncorrectNumberOfIdentifiers); } + if !helpers.contains(&key_package_i.identifier) { + return Err(Error::UnknownIdentifier); + } let xset: BTreeSet<_> = helpers.iter().cloned().collect(); if xset.len() != helpers.len() { return Err(Error::DuplicatedIdentifier); @@ -40,7 +130,7 @@ pub fn repair_share_step_1( let rand_val: Vec> = generate_coefficients::(helpers.len() - 1, rng); - compute_last_random_value(&xset, share_i, &rand_val, participant) + compute_last_random_value(&xset, key_package_i, &rand_val, participant) } /// Compute the last delta value given the (generated uniformly at random) remaining ones @@ -49,19 +139,20 @@ pub fn repair_share_step_1( /// Returns a BTreeMap mapping which value should be sent to which participant. fn compute_last_random_value( helpers: &BTreeSet>, - share_i: &SecretShare, + key_package_i: &KeyPackage, random_values: &Vec>, participant: Identifier, -) -> Result, Scalar>, Error> { +) -> Result, Delta>, Error> { // Calculate Lagrange Coefficient for helper_i - let zeta_i = compute_lagrange_coefficient(helpers, Some(participant), share_i.identifier)?; + let zeta_i = + compute_lagrange_coefficient(helpers, Some(participant), key_package_i.identifier)?; - let lhs = zeta_i * share_i.signing_share.0; + let lhs = zeta_i * key_package_i.signing_share.to_scalar(); - let mut out: BTreeMap, Scalar> = helpers + let mut out: BTreeMap, Delta> = helpers .iter() .copied() - .zip(random_values.iter().copied()) + .zip(random_values.iter().map(|v| Delta::new(*v))) .collect(); let mut sum_i_deltas = <::Field>::zero(); @@ -72,58 +163,56 @@ fn compute_last_random_value( out.insert( *helpers.last().ok_or(Error::IncorrectNumberOfIdentifiers)?, - lhs - sum_i_deltas, + Delta::new(lhs - sum_i_deltas), ); Ok(out) } -/// Communication round -/// -/// `helper_i` sends 1 `delta_j` to all other helpers (j) -/// `helper_i` retains 1 `delta_j` - -/// Step 2 of RTS. +/// Part 2 of RTS. /// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. -/// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { let mut sigma_j = <::Field>::zero(); - for d in deltas_j { - sigma_j = sigma_j + *d; + for d in deltas { + sigma_j = sigma_j + d.to_scalar(); } - sigma_j + Sigma::new(sigma_j) } -/// Communication round +/// Part 3 of RTS. /// -/// `helper_j` sends 1 `sigma_j` to the `participant` repairing their share. - -/// Step 3 of RTS +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { + public_key_package: &PublicKeyPackage, +) -> Result, Error> { let mut share = <::Field>::zero(); for s in sigmas { - share = share + *s; + share = share + s.to_scalar(); } + let signing_share = SigningShare::new(share); + let verifying_share = signing_share.into(); - SecretShare { - header: Header::default(), + Ok(KeyPackage { + header: Default::default(), identifier, - signing_share: SigningShare(share), - commitment: commitment.clone(), - } + signing_share, + verifying_share, + verifying_key: *public_key_package.verifying_key(), + min_signers: public_key_package + .min_signers() + .ok_or(Error::InvalidMinSigners)?, + }) } diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index ba0ed59e0..364beabb0 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -1,3 +1,4 @@ +#![no_std] #![allow(non_snake_case)] // It's emitting false positives; see https://github.com/rust-lang/rust-clippy/issues/9413 #![allow(clippy::derive_partial_eq_without_eq)] @@ -5,22 +6,27 @@ #![forbid(unsafe_code)] #![deny(clippy::indexing_slicing)] #![deny(clippy::unwrap_used)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc = document_features::document_features!()] -use std::{ +#[macro_use] +extern crate alloc; + +use core::marker::PhantomData; + +use alloc::{ collections::{BTreeMap, BTreeSet}, - default::Default, fmt::{self, Debug}, - marker::PhantomData, + vec::Vec, }; use derive_getters::Getters; #[cfg(any(test, feature = "test-impl"))] use hex::FromHex; +use keys::PublicKeyPackage; use rand_core::{CryptoRng, RngCore}; +use serialization::SerializableScalar; use zeroize::Zeroize; pub mod batch; @@ -56,29 +62,30 @@ pub use verifying_key::VerifyingKey; /// A type refinement for the scalar field element representing the per-message _[challenge]_. /// -/// [challenge]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-signature-challenge-computa -#[derive(Clone)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct Challenge( - pub(crate) <::Field as Field>::Scalar, -); +/// [challenge]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa +#[derive(Copy, Clone)] +pub struct Challenge(pub(crate) <::Field as Field>::Scalar); impl Challenge where C: Ciphersuite, { /// Creates a challenge from a scalar. - #[cfg(feature = "internals")] - pub fn from_scalar( + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(dead_code)] + pub(crate) fn from_scalar( scalar: <<::Group as Group>::Field as Field>::Scalar, ) -> Self { Self(scalar) } /// Return the underlying scalar. - #[cfg(feature = "internals")] - pub fn to_scalar(self) -> <<::Group as Group>::Field as Field>::Scalar { + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { self.0 } } @@ -87,7 +94,7 @@ impl Debug for Challenge where C: Ciphersuite, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("Secret") .field(&hex::encode(<::Field>::serialize( &self.0, @@ -103,26 +110,32 @@ where /// /// This is the only invocation of the H2 hash function from the [RFC]. /// -/// [FROST]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-signature-challenge-computa -/// [RFC]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-3.2 +/// [FROST]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa +/// [RFC]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -fn challenge(R: &Element, verifying_key: &VerifyingKey, msg: &[u8]) -> Challenge +fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + msg: &[u8], +) -> Result, Error> where C: Ciphersuite, { - let mut preimage = vec![]; + let mut preimage = Vec::new(); - preimage.extend_from_slice(::serialize(R).as_ref()); - preimage.extend_from_slice(::serialize(&verifying_key.element).as_ref()); + preimage.extend_from_slice(::serialize(R)?.as_ref()); + preimage.extend_from_slice(::serialize(&verifying_key.to_element())?.as_ref()); preimage.extend_from_slice(msg); - Challenge(C::H2(&preimage[..])) + Ok(Challenge(C::H2(&preimage[..]))) } /// Generates a random nonzero scalar. /// /// It assumes that the Scalar Eq/PartialEq implementation is constant-time. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn random_nonzero(rng: &mut R) -> Scalar { loop { let scalar = <::Field>::random(rng); @@ -153,7 +166,7 @@ struct Header { serde(deserialize_with = "crate::serialization::ciphersuite_deserialize::<_, C>") )] ciphersuite: (), - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] phantom: PhantomData, } @@ -177,17 +190,15 @@ where /// /// #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct BindingFactor(Scalar); +pub struct BindingFactor(Scalar); impl BindingFactor where C: Ciphersuite, { /// Serializes [`BindingFactor`] to bytes. - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.0) + pub fn serialize(&self) -> Vec { + SerializableScalar::(self.0).serialize() } } @@ -204,9 +215,7 @@ where /// A list of binding factors and their associated identifiers. #[derive(Clone)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct BindingFactorList(BTreeMap, BindingFactor>); +pub struct BindingFactorList(BTreeMap, BindingFactor>); impl BindingFactorList where @@ -226,20 +235,20 @@ where /// [`compute_binding_factors`] in the spec /// -/// [`compute_binding_factors`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.4 +/// [`compute_binding_factors`]: https://datatracker.ietf.org/doc/html/rfc9591#name-binding-factors-computation #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn compute_binding_factor_list( signing_package: &SigningPackage, verifying_key: &VerifyingKey, additional_prefix: &[u8], -) -> BindingFactorList +) -> Result, Error> where C: Ciphersuite, { - let preimages = signing_package.binding_factor_preimages(verifying_key, additional_prefix); + let preimages = signing_package.binding_factor_preimages(verifying_key, additional_prefix)?; - BindingFactorList( + Ok(BindingFactorList( preimages .iter() .map(|(identifier, preimage)| { @@ -247,7 +256,7 @@ where (*identifier, BindingFactor(binding_factor)) }) .collect(), - ) + )) } #[cfg(any(test, feature = "test-impl"))] @@ -260,12 +269,13 @@ where fn from_hex>(hex: T) -> Result { let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; - match v.try_into() { + let ret = match v.as_slice().try_into() { Ok(bytes) => <::Field>::deserialize(&bytes) .map(|scalar| Self(scalar)) .map_err(|_| "malformed scalar encoding"), Err(_) => Err("malformed scalar encoding"), - } + }; + ret } } @@ -302,12 +312,12 @@ fn compute_lagrange_coefficient( } if let Some(x) = x { - num *= x - *x_j; - den *= x_i - *x_j; + num = num * (x.to_scalar() - x_j.to_scalar()); + den = den * (x_i.to_scalar() - x_j.to_scalar()); } else { // Both signs inverted just to avoid requiring Neg (-*xj) - num *= *x_j; - den *= *x_j - x_i; + num = num * x_j.to_scalar(); + den = den * (x_j.to_scalar() - x_i.to_scalar()); } } if !x_i_found { @@ -324,7 +334,7 @@ fn compute_lagrange_coefficient( /// /// Implements [`derive_interpolating_value()`] from the spec. /// -/// [`derive_interpolating_value()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-polynomials +/// [`derive_interpolating_value()`]: https://datatracker.ietf.org/doc/html/rfc9591#name-polynomials #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] fn derive_interpolating_value( @@ -399,37 +409,39 @@ where // We separate this out into its own method so it can be tested #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(clippy::type_complexity)] pub fn binding_factor_preimages( &self, verifying_key: &VerifyingKey, additional_prefix: &[u8], - ) -> Vec<(Identifier, Vec)> { - let mut binding_factor_input_prefix = vec![]; + ) -> Result, Vec)>, Error> { + let mut binding_factor_input_prefix = Vec::new(); - // The length of a serialized verifying key of the same cipersuite does + // The length of a serialized verifying key of the same ciphersuite does // not change between runs of the protocol, so we don't need to hash to // get a fixed length. - binding_factor_input_prefix.extend_from_slice(verifying_key.serialize().as_ref()); + binding_factor_input_prefix.extend_from_slice(verifying_key.serialize()?.as_ref()); // The message is hashed with H4 to force the variable-length message // into a fixed-length byte string, same for hashing the variable-sized // (between runs of the protocol) set of group commitments, but with H5. binding_factor_input_prefix.extend_from_slice(C::H4(self.message.as_slice()).as_ref()); binding_factor_input_prefix.extend_from_slice( - C::H5(&round1::encode_group_commitments(self.signing_commitments())[..]).as_ref(), + C::H5(&round1::encode_group_commitments(self.signing_commitments())?[..]).as_ref(), ); binding_factor_input_prefix.extend_from_slice(additional_prefix); - self.signing_commitments() + Ok(self + .signing_commitments() .keys() .map(|identifier| { - let mut binding_factor_input = vec![]; + let mut binding_factor_input = Vec::new(); binding_factor_input.extend_from_slice(&binding_factor_input_prefix); binding_factor_input.extend_from_slice(identifier.serialize().as_ref()); (*identifier, binding_factor_input) }) - .collect() + .collect()) } } @@ -452,19 +464,24 @@ where /// The product of all signers' individual commitments, published as part of the /// final signature. #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct GroupCommitment(pub(crate) Element); +pub struct GroupCommitment(pub(crate) Element); impl GroupCommitment where C: Ciphersuite, { /// Return the underlying element. - #[cfg(feature = "internals")] - pub fn to_element(self) -> ::Element { + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_element(self) -> ::Element { self.0 } + + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn from_element(element: Element) -> Self { + Self(element) + } } /// Generates the group commitment which is published as part of the joint @@ -472,7 +489,7 @@ where /// /// Implements [`compute_group_commitment`] from the spec. /// -/// [`compute_group_commitment`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.5 +/// [`compute_group_commitment`]: https://datatracker.ietf.org/doc/html/rfc9591#name-group-commitment-computatio #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] fn compute_group_commitment( @@ -496,7 +513,7 @@ where for (commitment_identifier, commitment) in signing_package.signing_commitments() { // The following check prevents a party from accidentally revealing their share. // Note that the '&&' operator would be sufficient. - if identity == commitment.binding.0 || identity == commitment.hiding.0 { + if identity == commitment.binding.value() || identity == commitment.hiding.value() { return Err(Error::IdentityCommitment); } @@ -506,10 +523,10 @@ where // Collect the binding commitments and their binding factors for one big // multiscalar multiplication at the end. - binding_elements.push(commitment.binding.0); + binding_elements.push(commitment.binding.value()); binding_scalars.push(binding_factor.0); - group_commitment = group_commitment + commitment.hiding.0; + group_commitment = group_commitment + commitment.hiding.value(); } let accumulated_binding_commitment: Element = @@ -531,7 +548,8 @@ where /// [`round2::SignatureShare`] they sent. These identifiers must come from whatever mapping /// the coordinator has between communication channels and participants, i.e. /// they must have assurance that the [`round2::SignatureShare`] came from -/// the participant with that identifier. +/// the participant with that identifier. (This means that you *MUST NOT* send +/// the identifier along with the [`round2::SignatureShare`].) /// /// This operation is performed by a coordinator that can communicate with all /// the signing participants before publishing the final signature. The @@ -542,12 +560,45 @@ where /// signature, if the coordinator themselves is a signer and misbehaves, they /// can avoid that step. However, at worst, this results in a denial of /// service attack due to publishing an invalid signature. - pub fn aggregate( signing_package: &SigningPackage, signature_shares: &BTreeMap, round2::SignatureShare>, pubkeys: &keys::PublicKeyPackage, ) -> Result, Error> +where + C: Ciphersuite, +{ + aggregate_custom( + signing_package, + signature_shares, + pubkeys, + CheaterDetection::FirstCheater, + ) +} + +/// The type of cheater detection to use. +pub enum CheaterDetection { + /// Disable cheater detection. Fast in case there are invalid + /// shares. + Disabled, + /// Detect the first cheater and stop. Performance will depend on where + /// the cheater's share is in the list. + FirstCheater, + /// Detect all cheaters. Slower since all shares must be verified. + /// Performance will be proportional on the size of participants. + AllCheaters, +} + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. If you are disabling cheater detection, then the identifiers +/// in `signature_shares` do not need to correspond to the senders (i.e. +/// you don't need to authenticate the origin of the shares). +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, round2::SignatureShare>, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result, Error> where C: Ciphersuite, { @@ -556,33 +607,48 @@ where if signing_package.signing_commitments().len() != signature_shares.len() { return Err(Error::UnknownIdentifier); } - if !signing_package.signing_commitments().keys().all(|id| { - #[cfg(feature = "cheater-detection")] - return signature_shares.contains_key(id) && pubkeys.verifying_shares().contains_key(id); - #[cfg(not(feature = "cheater-detection"))] - return signature_shares.contains_key(id); - }) { + + if let Some(min) = pubkeys.min_signers() { + if signature_shares.len() < min as usize { + return Err(Error::IncorrectNumberOfShares); + } + } + + if !signing_package + .signing_commitments() + .keys() + .all(|id| match cheater_detection { + CheaterDetection::Disabled => signature_shares.contains_key(id), + CheaterDetection::FirstCheater | CheaterDetection::AllCheaters => { + signature_shares.contains_key(id) && pubkeys.verifying_shares().contains_key(id) + } + }) + { return Err(Error::UnknownIdentifier); } + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, signature_shares, pubkeys)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &pubkeys.verifying_key, &[]); + compute_binding_factor_list(&signing_package, &pubkeys.verifying_key, &[])?; // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let signing_package = ::pre_commitment_aggregate(&signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // The aggregation of the signature shares by summing them up, resulting in // a plain Schnorr signature. // // Implements [`aggregate`] from the spec. // - // [`aggregate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-5.3 + // [`aggregate`]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation let mut z = <::Field>::zero(); for signature_share in signature_shares.values() { - z = z + signature_share.share; + z = z + signature_share.to_scalar(); } let signature = Signature { @@ -598,53 +664,188 @@ where // Only if the verification of the aggregate signature failed; verify each share to find the cheater. // This approach is more efficient since we don't need to verify all shares // if the aggregate signature is valid (which should be the common case). - #[cfg(feature = "cheater-detection")] - if let Err(err) = verification_result { - // Compute the per-message challenge. - let challenge = crate::challenge::( - &group_commitment.0, - &pubkeys.verifying_key, - signing_package.message().as_slice(), - ); - - // Verify the signature shares. - for (signature_share_identifier, signature_share) in signature_shares { - // Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_, - // and where s[i] is a secret share of the constant term of _f_, the secret polynomial. - let signer_pubkey = pubkeys - .verifying_shares - .get(signature_share_identifier) - .ok_or(Error::UnknownIdentifier)?; - - // Compute Lagrange coefficient. - let lambda_i = derive_interpolating_value(signature_share_identifier, signing_package)?; - - let binding_factor = binding_factor_list - .get(signature_share_identifier) - .ok_or(Error::UnknownIdentifier)?; - - // Compute the commitment share. - let R_share = signing_package - .signing_commitment(signature_share_identifier) - .ok_or(Error::UnknownIdentifier)? - .to_group_commitment_share(binding_factor); - - // Compute relation values to verify this signature share. - signature_share.verify( - *signature_share_identifier, - &R_share, - signer_pubkey, - lambda_i, - &challenge, - )?; + match cheater_detection { + CheaterDetection::Disabled => { + verification_result?; + } + CheaterDetection::FirstCheater | CheaterDetection::AllCheaters => { + if verification_result.is_err() { + detect_cheater( + &group_commitment, + &pubkeys, + &signing_package, + &signature_shares, + &binding_factor_list, + cheater_detection, + )?; + } } + } + + Ok(signature) +} - // We should never reach here; but we return the verification error to be safe. - return Err(err); +/// Optional cheater detection feature +/// Each share is verified to find the cheater +fn detect_cheater( + group_commitment: &GroupCommitment, + pubkeys: &keys::PublicKeyPackage, + signing_package: &SigningPackage, + signature_shares: &BTreeMap, round2::SignatureShare>, + binding_factor_list: &BindingFactorList, + cheater_detection: CheaterDetection, +) -> Result<(), Error> { + // Compute the per-message challenge. + let challenge = ::challenge( + &group_commitment.0, + &pubkeys.verifying_key, + signing_package.message(), + )?; + + let mut all_culprits = Vec::new(); + + // Verify the signature shares. + for (identifier, signature_share) in signature_shares { + // Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_, + // and where s[i] is a secret share of the constant term of _f_, the secret polynomial. + let verifying_share = pubkeys + .verifying_shares + .get(identifier) + .ok_or(Error::UnknownIdentifier)?; + + let r = verify_signature_share_precomputed( + *identifier, + signing_package, + binding_factor_list, + group_commitment, + signature_share, + verifying_share, + challenge, + ); + match r { + Ok(_) => {} + Err(Error::InvalidSignatureShare { culprits }) => { + all_culprits.extend(culprits); + if let CheaterDetection::FirstCheater = cheater_detection { + break; + } + } + Err(e) => return Err(e), + } + } + if !all_culprits.is_empty() { + return Err(Error::InvalidSignatureShare { + culprits: all_culprits, + }); } - #[cfg(not(feature = "cheater-detection"))] - verification_result?; + // We should never reach here; but we return an error to be safe. + Err(Error::InvalidSignature) +} - Ok(signature) +/// Verify a signature share for the given participant `identifier`, +/// `verifying_share` and `signature_share`; with the `signing_package` +/// for which the signature share was produced and with the group's +/// `verifying_key`. +/// +/// This is not required for regular FROST usage but might be useful in certain +/// situations where it is desired to verify each individual signature share +/// before aggregating the signature. +pub fn verify_signature_share( + identifier: Identifier, + verifying_share: &keys::VerifyingShare, + signature_share: &round2::SignatureShare, + signing_package: &SigningPackage, + verifying_key: &VerifyingKey, +) -> Result<(), Error> { + // In order to reuse `pre_aggregate()`, we need to create some "dummy" containers + let signature_shares = BTreeMap::from([(identifier, *signature_share)]); + let verifying_shares = BTreeMap::from([(identifier, *verifying_share)]); + let public_key_package = PublicKeyPackage { + verifying_shares, + verifying_key: *verifying_key, + // Use None since we don't have the min_signers value here. This + // can only cause problems if the `pre_aggregate` function relies on it. + // This has been documented in `pre_aggregate()`. + min_signers: None, + header: Header::default(), + }; + + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, &signature_shares, &public_key_package)?; + + // Extract the processed values back from the "dummy" containers + let verifying_share = pubkeys + .verifying_shares() + .get(&identifier) + .ok_or(Error::UnknownIdentifier)?; + let verifying_key = pubkeys.verifying_key(); + let signature_share = signature_shares + .get(&identifier) + .ok_or(Error::UnknownIdentifier)?; + + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the + // binding factor. + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(&signing_package, verifying_key, &[])?; + + let signing_package = ::pre_commitment_aggregate(&signing_package, &binding_factor_list)?; + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; + + // Compute the per-message challenge. + let challenge = ::challenge( + &group_commitment.clone().to_element(), + verifying_key, + signing_package.message().as_slice(), + )?; + + verify_signature_share_precomputed( + identifier, + &signing_package, + &binding_factor_list, + &group_commitment, + signature_share, + verifying_share, + challenge, + ) +} + +/// Similar to [`verify_signature_share()`] but using a precomputed +/// `binding_factor_list` and `challenge`. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn verify_signature_share_precomputed( + signature_share_identifier: Identifier, + signing_package: &SigningPackage, + binding_factor_list: &BindingFactorList, + group_commitment: &GroupCommitment, + signature_share: &round2::SignatureShare, + verifying_share: &keys::VerifyingShare, + challenge: Challenge, +) -> Result<(), Error> { + let lambda_i = derive_interpolating_value(&signature_share_identifier, signing_package)?; + + let binding_factor = binding_factor_list + .get(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)?; + + let R_share = signing_package + .signing_commitment(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)? + .to_group_commitment_share(binding_factor); + + // Compute relation values to verify this signature share. + ::verify_share( + group_commitment, + signature_share, + signature_share_identifier, + &R_share, + verifying_share, + lambda_i, + &challenge, + )?; + + Ok(()) } diff --git a/frost-core/src/round1.rs b/frost-core/src/round1.rs index fd9faa96a..d2468fde6 100644 --- a/frost-core/src/round1.rs +++ b/frost-core/src/round1.rs @@ -1,8 +1,12 @@ //! FROST Round 1 functionality and types +// Remove after https://github.com/rust-lang/rust/issues/147648 is fixed +#![allow(unused_assignments)] -use std::{ +use alloc::{ collections::BTreeMap, fmt::{self, Debug}, + string::ToString, + vec::Vec, }; use derive_getters::Getters; @@ -10,16 +14,15 @@ use derive_getters::Getters; use hex::FromHex; use rand_core::{CryptoRng, RngCore}; -use zeroize::Zeroize; +use zeroize::{Zeroize, ZeroizeOnDrop}; -use crate as frost; use crate::{ - serialization::{Deserialize, Serialize}, - Ciphersuite, Element, Error, Field, Group, Header, Scalar, + serialization::{SerializableElement, SerializableScalar}, + Ciphersuite, Element, Error, Field, Group, Header, }; -#[cfg(feature = "serde")] -use crate::serialization::{ElementSerialization, ScalarSerialization}; +#[cfg(feature = "serialization")] +use crate::serialization::{Deserialize, Serialize}; use super::{keys::SigningShare, Identifier}; @@ -27,9 +30,8 @@ use super::{keys::SigningShare, Identifier}; #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ScalarSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ScalarSerialization"))] -pub struct Nonce(pub(super) Scalar); +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Nonce(pub(super) SerializableScalar); impl Nonce where @@ -43,7 +45,7 @@ where /// /// An implementation of `nonce_generate(secret)` from the [spec]. /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-nonce-generation + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#name-nonce-generation pub fn new(secret: &SigningShare, rng: &mut R) -> Self where R: CryptoRng + RngCore, @@ -54,35 +56,47 @@ where Self::nonce_generate_from_random_bytes(secret, random_bytes) } + /// Create a nonce from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + fn from_scalar(scalar: <<::Group as Group>::Field as Field>::Scalar) -> Self { + Self(SerializableScalar(scalar)) + } + + /// Convert a nonce into a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { + self.0 .0 + } + /// Generates a nonce from the given random bytes. /// This function allows testing and MUST NOT be made public. pub(crate) fn nonce_generate_from_random_bytes( secret: &SigningShare, random_bytes: [u8; 32], ) -> Self { - let secret_enc = <::Field>::serialize(&secret.0); + let secret_enc = secret.0.serialize(); let input: Vec = random_bytes .iter() - .chain(secret_enc.as_ref().iter()) + .chain(secret_enc.iter()) .cloned() .collect(); - Self(C::H3(input.as_slice())) + Self::from_scalar(C::H3(input.as_slice())) } /// Deserialize [`Nonce`] from bytes - pub fn deserialize( - bytes: <::Field as Field>::Serialization, - ) -> Result> { - <::Field>::deserialize(&bytes) - .map(|scalar| Self(scalar)) - .map_err(|e| e.into()) + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) } /// Serialize [`Nonce`] to bytes - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.0) + pub fn serialize(&self) -> Vec { + self.0.serialize() } } @@ -91,7 +105,7 @@ where C: Ciphersuite, { fn zeroize(&mut self) { - *self = Nonce(<::Field>::zero()); + *self = Nonce::from_scalar(<::Field>::zero()); } } @@ -104,32 +118,7 @@ where fn from_hex>(hex: T) -> Result { let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; - match v.try_into() { - Ok(bytes) => Self::deserialize(bytes).map_err(|_| "malformed nonce encoding"), - Err(_) => Err("malformed nonce encoding"), - } - } -} - -#[cfg(feature = "serde")] -impl TryFrom> for Nonce -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ScalarSerialization) -> Result { - Self::deserialize(value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ScalarSerialization -where - C: Ciphersuite, -{ - fn from(value: Nonce) -> Self { - Self(value.serialize()) + Self::deserialize(&v).map_err(|_| "malformed nonce encoding") } } @@ -137,46 +126,34 @@ where #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ElementSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ElementSerialization"))] -pub struct NonceCommitment(pub(super) Element); +pub struct NonceCommitment(pub(super) SerializableElement); impl NonceCommitment where C: Ciphersuite, { - /// Deserialize [`NonceCommitment`] from bytes - pub fn deserialize(bytes: ::Serialization) -> Result> { - ::deserialize(&bytes) - .map(|element| Self(element)) - .map_err(|e| e.into()) + /// Create a new [`NonceCommitment`] from an [`Element`] + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(value: Element) -> Self { + Self(SerializableElement(value)) } - /// Serialize [`NonceCommitment`] to bytes - pub fn serialize(&self) -> ::Serialization { - ::serialize(&self.0) + /// Get the inner [`Element`] of the [`NonceCommitment`] + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn value(&self) -> Element { + self.0 .0 } -} -#[cfg(feature = "serde")] -impl TryFrom> for NonceCommitment -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ElementSerialization) -> Result { - Self::deserialize(value.0) + /// Deserialize [`NonceCommitment`] from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) } -} -#[cfg(feature = "serde")] -impl From> for ElementSerialization -where - C: Ciphersuite, -{ - fn from(value: NonceCommitment) -> Self { - Self(value.serialize()) + /// Serialize [`NonceCommitment`] to bytes + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() } } @@ -186,7 +163,12 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("NonceCommitment") - .field(&hex::encode(self.serialize())) + .field( + &self + .serialize() + .map(hex::encode) + .unwrap_or("".to_string()), + ) .finish() } } @@ -205,7 +187,7 @@ where C: Ciphersuite, { fn from(nonce: &Nonce) -> Self { - Self(::generator() * nonce.0) + Self::new(::generator() * nonce.to_scalar()) } } @@ -218,12 +200,7 @@ where fn from_hex>(hex: T) -> Result { let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; - match v.try_into() { - Ok(bytes) => { - Self::deserialize(bytes).map_err(|_| "malformed nonce commitment encoding") - } - Err(_) => Err("malformed nonce commitment encoding"), - } + Self::deserialize(&v).map_err(|_| "malformed nonce commitment encoding") } } @@ -232,7 +209,7 @@ where /// Note that [`SigningNonces`] must be used *only once* for a signing /// operation; re-using nonces will result in leakage of a signer's long-lived /// signing key. -#[derive(Clone, Zeroize, PartialEq, Eq, Getters)] +#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq, Eq, Getters)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] @@ -295,7 +272,7 @@ impl Debug for SigningNonces where C: Ciphersuite, { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SigningNonces") .field("hiding", &"") .field("binding", &"") @@ -350,16 +327,16 @@ where } } - /// Computes the [signature commitment share] from these round one signing commitments. + /// Computes the [commitment share] from these round one signing commitments. /// - /// [signature commitment share]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-signature-share-verificatio + /// [commitment share]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(super) fn to_group_commitment_share( self, - binding_factor: &frost::BindingFactor, + binding_factor: &crate::BindingFactor, ) -> GroupCommitmentShare { - GroupCommitmentShare::(self.hiding.0 + (self.binding.0 * binding_factor.0)) + GroupCommitmentShare::(self.hiding.value() + (self.binding.value() * binding_factor.0)) } } @@ -393,6 +370,21 @@ where #[derive(Clone, Copy, PartialEq)] pub struct GroupCommitmentShare(pub(super) Element); +impl GroupCommitmentShare { + /// Create from an element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[allow(unused)] + pub(crate) fn from_element(element: Element) -> Self { + Self(element) + } + + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn to_element(self) -> Element { + self.0 + } +} + /// Encode the list of group signing commitments. /// /// Implements [`encode_group_commitment_list()`] from the spec. @@ -403,19 +395,21 @@ pub struct GroupCommitmentShare(pub(super) Element); /// Returns a byte string containing the serialized representation of the /// commitment list. /// -/// [`encode_group_commitment_list()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-list-operations +/// [`encode_group_commitment_list()`]: https://datatracker.ietf.org/doc/html/rfc9591#name-list-operations +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(super) fn encode_group_commitments( signing_commitments: &BTreeMap, SigningCommitments>, -) -> Vec { +) -> Result, Error> { let mut bytes = vec![]; for (item_identifier, item) in signing_commitments { bytes.extend_from_slice(item_identifier.serialize().as_ref()); - bytes.extend_from_slice(::serialize(&item.hiding.0).as_ref()); - bytes.extend_from_slice(::serialize(&item.binding.0).as_ref()); + bytes.extend_from_slice(::serialize(&item.hiding.value())?.as_ref()); + bytes.extend_from_slice(::serialize(&item.binding.value())?.as_ref()); } - bytes + Ok(bytes) } /// Done once by each participant, to generate _their_ nonces and commitments @@ -457,7 +451,7 @@ where /// Generates the signing nonces and commitments to be used in the signing /// operation. /// -/// [`commit`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-round-one-commitment +/// [`commit`]: https://datatracker.ietf.org/doc/html/rfc9591#name-round-one-commitment pub fn commit( secret: &SigningShare, rng: &mut R, diff --git a/frost-core/src/round2.rs b/frost-core/src/round2.rs index 509a6e022..b7a1e909d 100644 --- a/frost-core/src/round2.rs +++ b/frost-core/src/round2.rs @@ -1,78 +1,56 @@ //! FROST Round 2 functionality and types, for signature share generation -use std::fmt::{self, Debug}; +use core::fmt::{self, Debug}; use crate as frost; use crate::{ - challenge, Challenge, Ciphersuite, Error, Field, Group, {round1, *}, + Challenge, Ciphersuite, Error, Field, Group, {round1, *}, }; -#[cfg(feature = "serde")] -use crate::serialization::ScalarSerialization; - -// Used to help encoding a SignatureShare. Since it has a Scalar it can't -// be directly encoded with serde, so we use this struct to wrap the scalar. -#[cfg(feature = "serde")] -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ScalarSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ScalarSerialization"))] -struct SignatureShareHelper(Scalar); - -#[cfg(feature = "serde")] -impl TryFrom> for SignatureShareHelper -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ScalarSerialization) -> Result { - <::Field>::deserialize(&value.0) - .map(|scalar| Self(scalar)) - .map_err(|e| e.into()) - } -} - -#[cfg(feature = "serde")] -impl From> for ScalarSerialization -where - C: Ciphersuite, -{ - fn from(value: SignatureShareHelper) -> Self { - Self(<::Field>::serialize(&value.0)) - } -} - /// A participant's signature share, which the coordinator will aggregate with all other signer's /// shares into the joint signature. #[derive(Clone, Copy, Eq, PartialEq, Getters)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] -#[cfg_attr(feature = "serde", serde(try_from = "SignatureShareSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "SignatureShareSerialization"))] pub struct SignatureShare { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, /// This participant's signature over the message. - pub(crate) share: Scalar, + pub(crate) share: SerializableScalar, } impl SignatureShare where C: Ciphersuite, { + pub(crate) fn new( + scalar: <<::Group as Group>::Field as Field>::Scalar, + ) -> Self { + Self { + header: Header::default(), + share: SerializableScalar(scalar), + } + } + + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { + self.share.0 + } + /// Deserialize [`SignatureShare`] from bytes - pub fn deserialize( - bytes: <::Field as Field>::Serialization, - ) -> Result> { - <::Field>::deserialize(&bytes) - .map(|scalar| Self { share: scalar }) - .map_err(|e| e.into()) + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self { + header: Header::default(), + share: SerializableScalar::deserialize(bytes)?, + }) } /// Serialize [`SignatureShare`] to bytes - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.share) + pub fn serialize(&self) -> Vec { + self.share.serialize() } /// Tests if a signature share issued by a participant is valid before @@ -80,7 +58,7 @@ where /// /// This is the final step of [`verify_signature_share`] from the spec. /// - /// [`verify_signature_share`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-signature-share-verificatio + /// [`verify_signature_share`]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn verify( @@ -91,11 +69,12 @@ where lambda_i: Scalar, challenge: &Challenge, ) -> Result<(), Error> { - if (::generator() * self.share) - != (group_commitment_share.0 + (verifying_share.0 * challenge.0 * lambda_i)) + if (::generator() * self.to_scalar()) + != (group_commitment_share.to_element() + + (verifying_share.to_element() * challenge.0 * lambda_i)) { return Err(Error::InvalidSignatureShare { - culprit: identifier, + culprits: vec![identifier], }); } @@ -103,41 +82,6 @@ where } } -#[cfg(feature = "serde")] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] -struct SignatureShareSerialization { - /// Serialization header - pub(crate) header: Header, - share: SignatureShareHelper, -} - -#[cfg(feature = "serde")] -impl From> for SignatureShare -where - C: Ciphersuite, -{ - fn from(value: SignatureShareSerialization) -> Self { - Self { - share: value.share.0, - } - } -} - -#[cfg(feature = "serde")] -impl From> for SignatureShareSerialization -where - C: Ciphersuite, -{ - fn from(value: SignatureShare) -> Self { - Self { - header: Header::default(), - share: SignatureShareHelper(value.share), - } - } -} - impl Debug for SignatureShare where C: Ciphersuite, @@ -152,18 +96,18 @@ where /// Compute the signature share for a signing operation. #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -fn compute_signature_share( +pub(super) fn compute_signature_share( signer_nonces: &round1::SigningNonces, binding_factor: BindingFactor, lambda_i: <<::Group as Group>::Field as Field>::Scalar, key_package: &keys::KeyPackage, challenge: Challenge, ) -> SignatureShare { - let z_share: <::Field as Field>::Scalar = signer_nonces.hiding.0 - + (signer_nonces.binding.0 * binding_factor.0) - + (lambda_i * key_package.signing_share.0 * challenge.0); + let z_share: <::Field as Field>::Scalar = signer_nonces.hiding.to_scalar() + + (signer_nonces.binding.to_scalar() * binding_factor.0) + + (lambda_i * key_package.signing_share.to_scalar() * challenge.to_scalar()); - SignatureShare:: { share: z_share } + SignatureShare::::new(z_share) } /// Performed once by each participant selected for the signing operation. @@ -177,7 +121,7 @@ fn compute_signature_share( /// Assumes the participant has already determined which nonce corresponds with /// the commitment that was assigned by the coordinator in the SigningPackage. /// -/// [`sign`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-round-two-signature-share-g +/// [`sign`]: https://datatracker.ietf.org/doc/html/rfc9591#name-round-two-signature-share-g pub fn sign( signing_package: &SigningPackage, signer_nonces: &round1::SigningNonces, @@ -198,34 +142,40 @@ pub fn sign( return Err(Error::IncorrectCommitment); } + let (signing_package, signer_nonces, key_package) = + ::pre_sign(signing_package, signer_nonces, key_package)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &key_package.verifying_key, &[]); + compute_binding_factor_list(&signing_package, &key_package.verifying_key, &[])?; let binding_factor: frost::BindingFactor = binding_factor_list .get(&key_package.identifier) .ok_or(Error::UnknownIdentifier)? .clone(); // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let (signing_package, signer_nonces) = + ::pre_commitment_sign(&signing_package, &signer_nonces, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // Compute Lagrange coefficient. - let lambda_i = frost::derive_interpolating_value(key_package.identifier(), signing_package)?; + let lambda_i = frost::derive_interpolating_value(key_package.identifier(), &signing_package)?; // Compute the per-message challenge. - let challenge = challenge::( + let challenge = ::challenge( &group_commitment.0, &key_package.verifying_key, - signing_package.message.as_slice(), - ); + signing_package.message(), + )?; // Compute the Schnorr signature share. - let signature_share = compute_signature_share( - signer_nonces, + let signature_share = ::compute_signature_share( + &group_commitment, + &signer_nonces, binding_factor, lambda_i, - key_package, + &key_package, challenge, ); diff --git a/frost-core/src/scalar_mul.rs b/frost-core/src/scalar_mul.rs index ea441ab1f..3d82b28d0 100644 --- a/frost-core/src/scalar_mul.rs +++ b/frost-core/src/scalar_mul.rs @@ -1,37 +1,18 @@ -//! Non-adjacent form (NAF) implementations for fast batch scalar multiplcation +//! Non-adjacent form (NAF) implementations for fast batch scalar multiplication // We expect slicings in this module to never panic due to algorithmic // constraints. #![allow(clippy::indexing_slicing)] -use std::{ +use core::{ borrow::Borrow, fmt::{Debug, Result}, marker::PhantomData, }; -use crate::{Ciphersuite, Element, Field, Group, Scalar}; +use alloc::vec::Vec; -/// Calculates the quotient of `self` and `rhs`, rounding the result towards positive infinity. -/// -/// # Panics -/// -/// This function will panic if `rhs` is 0 or the division results in overflow. -/// -/// This function is similar to `div_ceil` that is [available on -/// Nightly](https://github.com/rust-lang/rust/issues/88581). -/// -// TODO: remove this function and use `div_ceil()` instead when `int_roundings` -// is stabilized. -const fn div_ceil(lhs: usize, rhs: usize) -> usize { - let d = lhs / rhs; - let r = lhs % rhs; - if r > 0 && rhs > 0 { - d + 1 - } else { - d - } -} +use crate::{Ciphersuite, Element, Field, Group, Scalar}; /// A trait for transforming a scalar generic over a ciphersuite to a non-adjacent form (NAF). pub trait NonAdjacentForm { @@ -79,7 +60,7 @@ where let mut naf = vec![0; naf_length]; // Get the number of 64-bit limbs we need. - let num_limbs: usize = div_ceil(naf_length, u64::BITS as usize); + let num_limbs: usize = naf_length.div_ceil(u64::BITS as usize); let mut x_u64 = vec![0u64; num_limbs]; @@ -243,7 +224,7 @@ impl LookupTable5 { } impl Debug for LookupTable5 { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result { write!(f, "LookupTable5({:?})", self.bytes) } } diff --git a/frost-core/src/serialization.rs b/frost-core/src/serialization.rs index d98416f76..8e310fdf8 100644 --- a/frost-core/src/serialization.rs +++ b/frost-core/src/serialization.rs @@ -1,17 +1,63 @@ //! Serialization support. -use crate::{Ciphersuite, Error, Field, Group}; +#[cfg(feature = "serde")] +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +#[cfg(feature = "serde")] +use core::fmt::Formatter; +#[cfg(feature = "serde")] +use core::marker::PhantomData; +use zeroize::Zeroize; #[cfg(feature = "serde")] +use crate::keys::PublicKeyPackage; +#[cfg(feature = "serde")] +use crate::keys::VerifyingShare; +use crate::{Ciphersuite, FieldError}; +#[cfg(feature = "serde")] +use crate::{Header, Identifier, VerifyingKey}; + +use crate::{Element, Error, Field, Group}; + +#[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] /// Helper struct to serialize a Scalar. -pub(crate) struct ScalarSerialization( - pub <<::Group as Group>::Field as Field>::Serialization, +pub(crate) struct SerializableScalar( + pub <<::Group as Group>::Field as Field>::Scalar, ); +impl SerializableScalar +where + C: Ciphersuite, +{ + /// Serialize a Scalar. + pub fn serialize(&self) -> Vec { + <::Field>::serialize(&self.0) + .as_ref() + .to_vec() + } + + /// Deserialize a Scalar from a serialized buffer. + pub fn deserialize(bytes: &[u8]) -> Result> { + let serialized: <::Field as Field>::Serialization = + bytes.try_into().map_err(|_| FieldError::MalformedScalar)?; + let scalar = <::Field>::deserialize(&serialized)?; + Ok(Self(scalar)) + } +} + +impl Zeroize for SerializableScalar +where + C: Ciphersuite, +{ + fn zeroize(&mut self) { + self.0 = <::Field as Field>::zero(); + } +} + #[cfg(feature = "serde")] -impl serde::Serialize for ScalarSerialization +impl serde::Serialize for SerializableScalar where C: Ciphersuite, { @@ -19,12 +65,13 @@ where where S: serde::Serializer, { - serdect::array::serialize_hex_lower_or_bin(&self.0.as_ref(), serializer) + let serialized = <::Group as Group>::Field::serialize(&self.0); + serdect::array::serialize_hex_lower_or_bin(&serialized.as_ref(), serializer) } } #[cfg(feature = "serde")] -impl<'de, C> serde::Deserialize<'de> for ScalarSerialization +impl<'de, C> serde::Deserialize<'de> for SerializableScalar where C: Ciphersuite, { @@ -32,28 +79,42 @@ where where D: serde::Deserializer<'de>, { - // Get size from the size of the zero scalar + // Get serialization buffer from the zero scalar let zero = <::Field as Field>::zero(); - let len = <::Field as Field>::serialize(&zero) - .as_ref() - .len(); - - let mut bytes = vec![0u8; len]; - serdect::array::deserialize_hex_or_bin(&mut bytes[..], deserializer)?; - let array = bytes - .try_into() - .map_err(|_| serde::de::Error::custom("invalid byte length"))?; - Ok(Self(array)) + let mut serialization = <::Field as Field>::serialize(&zero); + + serdect::array::deserialize_hex_or_bin(serialization.as_mut(), deserializer)?; + + <::Group as Group>::Field::deserialize(&serialization) + .map(|scalar| Self(scalar)) + .map_err(serde::de::Error::custom) } } -#[cfg(feature = "serde")] -pub(crate) struct ElementSerialization( - pub(crate) <::Group as Group>::Serialization, -); +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct SerializableElement(pub(crate) Element); + +impl SerializableElement +where + C: Ciphersuite, +{ + /// Serialize an Element. Returns an error if it's the identity. + pub fn serialize(&self) -> Result, Error> { + Ok(::serialize(&self.0)?.as_ref().to_vec()) + } + + /// Deserialize an Element. Returns an error if it's malformed or is the + /// identity. + pub fn deserialize(bytes: &[u8]) -> Result> { + let serialized: ::Serialization = + bytes.try_into().map_err(|_| FieldError::MalformedScalar)?; + let scalar = ::deserialize(&serialized)?; + Ok(Self(scalar)) + } +} #[cfg(feature = "serde")] -impl serde::Serialize for ElementSerialization +impl serde::Serialize for SerializableElement where C: Ciphersuite, { @@ -61,12 +122,14 @@ where where S: serde::Serializer, { - serdect::array::serialize_hex_lower_or_bin(&self.0.as_ref(), serializer) + let serialized = + ::serialize(&self.0).map_err(serde::ser::Error::custom)?; + serdect::array::serialize_hex_lower_or_bin(&serialized.as_ref(), serializer) } } #[cfg(feature = "serde")] -impl<'de, C> serde::Deserialize<'de> for ElementSerialization +impl<'de, C> serde::Deserialize<'de> for SerializableElement where C: Ciphersuite, { @@ -74,21 +137,22 @@ where where D: serde::Deserializer<'de>, { - // Get size from the size of the generator + // Get serialization buffer from the generator let generator = ::generator(); - let len = ::serialize(&generator).as_ref().len(); - - let mut bytes = vec![0u8; len]; - serdect::array::deserialize_hex_or_bin(&mut bytes[..], deserializer)?; - let array = bytes - .try_into() - .map_err(|_| serde::de::Error::custom("invalid byte length"))?; - Ok(Self(array)) + let mut serialization = + ::serialize(&generator).expect("serializing the generator always works"); + + serdect::array::deserialize_hex_or_bin(serialization.as_mut(), deserializer)?; + + ::deserialize(&serialization) + .map(|element| Self(element)) + .map_err(serde::de::Error::custom) } } // The short 4-byte ID. Derived as the CRC-32 of the UTF-8 // encoded ID in big endian format. +#[cfg(feature = "serde")] const fn short_id() -> [u8; 4] where C: Ciphersuite, @@ -120,7 +184,7 @@ where C: Ciphersuite, { if deserializer.is_human_readable() { - let s: String = serde::de::Deserialize::deserialize(deserializer)?; + let s: alloc::string::String = serde::de::Deserialize::deserialize(deserializer)?; if s != C::ID { Err(serde::de::Error::custom("wrong ciphersuite")) } else { @@ -172,13 +236,13 @@ pub(crate) trait Deserialize { /// Deserialize the struct from a slice of bytes. fn deserialize(bytes: &[u8]) -> Result> where - Self: std::marker::Sized; + Self: core::marker::Sized; } #[cfg(feature = "serialization")] impl Serialize for T { fn serialize(&self) -> Result, Error> { - postcard::to_stdvec(self).map_err(|_| Error::SerializationError) + postcard::to_allocvec(self).map_err(|_| Error::SerializationError) } } @@ -188,3 +252,239 @@ impl serde::Deserialize<'de>, C: Ciphersuite> Deserialize for T { postcard::from_bytes(bytes).map_err(|_| Error::DeserializationError) } } + +/// Custom deserializer for PublicKeyPackage, which allows a non-existing +/// `min_signers` field for the `postcard` encoding. +#[cfg(feature = "serde")] +impl<'de, C: Ciphersuite> serde::Deserialize<'de> for PublicKeyPackage +where + C: Ciphersuite, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use core::fmt; + + // The following are copied from the `serde::Deserialize` derive, and + // are required to support `visit_map()` which in turn is required for + // `serde_json`. + + enum Field { + Field0, + Field1, + Field2, + Field3, + } + + struct FieldVisitor; + + impl<'de> serde::de::Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, __formatter: &mut Formatter) -> fmt::Result { + Formatter::write_str(__formatter, "field identifier") + } + + fn visit_u64<__E>(self, __value: u64) -> Result + where + __E: serde::de::Error, + { + match __value { + 0u64 => Ok(Field::Field0), + 1u64 => Ok(Field::Field1), + 2u64 => Ok(Field::Field2), + 3u64 => Ok(Field::Field3), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(__value), + &"field index 0 <= i < 4", + )), + } + } + + fn visit_str<__E>(self, __value: &str) -> Result + where + __E: serde::de::Error, + { + match __value { + "header" => Ok(Field::Field0), + "verifying_shares" => Ok(Field::Field1), + "verifying_key" => Ok(Field::Field2), + "min_signers" => Ok(Field::Field3), + _ => Err(serde::de::Error::unknown_field(__value, FIELDS)), + } + } + + fn visit_bytes<__E>(self, __value: &[u8]) -> Result + where + __E: serde::de::Error, + { + match __value { + b"header" => Ok(Field::Field0), + b"verifying_shares" => Ok(Field::Field1), + b"verifying_key" => Ok(Field::Field2), + b"min_signers" => Ok(Field::Field3), + _ => { + let __value = &alloc::string::String::from_utf8_lossy(__value); + Err(serde::de::Error::unknown_field(__value, FIELDS)) + } + } + } + } + + impl<'de> serde::Deserialize<'de> for Field { + #[inline] + fn deserialize<__D>(__deserializer: __D) -> Result + where + __D: serde::Deserializer<'de>, + { + serde::Deserializer::deserialize_identifier(__deserializer, FieldVisitor) + } + } + + struct Visitor { + marker: PhantomData, + } + + impl<'de, C: Ciphersuite> serde::de::Visitor<'de> for Visitor + where + C: Ciphersuite, + { + type Value = PublicKeyPackage; + + fn expecting(&self, fmt: &mut Formatter) -> core::fmt::Result { + Formatter::write_str(fmt, "struct PublicKeyPackage") + } + + // Postcard serializes structs as sequences, so we override + // `visit_seq` to deserialize the struct from a sequence of elements. + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + // Read the first three fields as usual. + + let header = seq.next_element::>()?.ok_or_else(|| { + serde::de::Error::invalid_length( + 0usize, + &"struct PublicKeyPackage with 4 elements", + ) + })?; + let verifying_shares = seq + .next_element::, VerifyingShare>>()? + .ok_or_else(|| { + serde::de::Error::invalid_length( + 1usize, + &"struct PublicKeyPackage with 4 elements", + ) + })?; + let verifying_key = seq.next_element::>()?.ok_or_else(|| { + serde::de::Error::invalid_length( + 2usize, + &"struct PublicKeyPackage with 4 elements", + ) + })?; + + // For the `min_signers` field, fill it with None if + // `next_element()` fails (i.e. there are no other elements) + let min_signers = match seq.next_element::>() { + Ok(Some(min_signers)) => min_signers, + _ => None, + }; + + Ok(PublicKeyPackage { + header, + verifying_shares, + verifying_key, + min_signers, + }) + } + + // Again this is copied from the `serde::Deserialize` derive; + // the only change is not requiring `min_signers` to be present. + fn visit_map<__A>(self, mut __map: __A) -> Result + where + __A: serde::de::MapAccess<'de>, + { + let mut __field0: Option> = None; + let mut __field1: Option, VerifyingShare>> = None; + let mut __field2: Option> = None; + let mut __field3: Option> = None; + while let Some(__key) = serde::de::MapAccess::next_key::(&mut __map)? { + match __key { + Field::Field0 => { + if Option::is_some(&__field0) { + return Err(<__A::Error as serde::de::Error>::duplicate_field( + "header", + )); + } + __field0 = + Some(serde::de::MapAccess::next_value::>(&mut __map)?); + } + Field::Field1 => { + if Option::is_some(&__field1) { + return Err(<__A::Error as serde::de::Error>::duplicate_field( + "verifying_shares", + )); + } + __field1 = Some(serde::de::MapAccess::next_value::< + BTreeMap, VerifyingShare>, + >(&mut __map)?); + } + Field::Field2 => { + if Option::is_some(&__field2) { + return Err(<__A::Error as serde::de::Error>::duplicate_field( + "verifying_key", + )); + } + __field2 = Some(serde::de::MapAccess::next_value::>( + &mut __map, + )?); + } + Field::Field3 => { + if Option::is_some(&__field3) { + return Err(<__A::Error as serde::de::Error>::duplicate_field( + "min_signers", + )); + } + __field3 = + Some(serde::de::MapAccess::next_value::>(&mut __map)?); + } + } + } + let __field0 = match __field0 { + Some(__field0) => __field0, + None => Err(<__A::Error as serde::de::Error>::missing_field("header"))?, + }; + let __field1 = match __field1 { + Some(__field1) => __field1, + None => Err(<__A::Error as serde::de::Error>::missing_field( + "verifying_shares", + ))?, + }; + let __field2 = match __field2 { + Some(__field2) => __field2, + None => Err(<__A::Error as serde::de::Error>::missing_field( + "verifying_key", + ))?, + }; + let __field3 = __field3.unwrap_or_default(); + Ok(PublicKeyPackage { + header: __field0, + verifying_shares: __field1, + verifying_key: __field2, + min_signers: __field3, + }) + } + } + + const FIELDS: &[&str] = &["header", "verifying_shares", "verifying_key", "min_signers"]; + deserializer.deserialize_struct( + "PublicKeyPackage", + FIELDS, + Visitor { + marker: PhantomData::, + }, + ) + } +} diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs index e913c7de1..02ccaac0d 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -1,11 +1,12 @@ //! Schnorr signatures over prime order groups (or subgroups) -use debugless_unwrap::DebuglessUnwrap; +use alloc::{string::ToString, vec::Vec}; +use derive_getters::Getters; use crate::{Ciphersuite, Element, Error, Field, Group, Scalar}; /// A Schnorr signature over some prime order group (or subgroup). -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Getters)] pub struct Signature { /// The commitment `R` to the signature nonce. pub(crate) R: Element, @@ -29,55 +30,67 @@ where Self { R, z } } - /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. - pub fn deserialize(bytes: C::SignatureSerialization) -> Result> { + /// Converts default-encoded bytes as + /// [`Ciphersuite::SignatureSerialization`] into a `Signature`. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn default_deserialize(bytes: &[u8]) -> Result> { // To compute the expected length of the encoded point, encode the generator // and get its length. Note that we can't use the identity because it can be encoded // shorter in some cases (e.g. P-256, which uses SEC1 encoding). let generator = ::generator(); - let mut R_bytes = Vec::from(::serialize(&generator).as_ref()); - - let R_bytes_len = R_bytes.len(); - - R_bytes[..].copy_from_slice( - bytes - .as_ref() - .get(0..R_bytes_len) - .ok_or(Error::MalformedSignature)?, - ); + let mut R_serialization = ::serialize(&generator)?; + let R_bytes_len = R_serialization.as_ref().len(); - let R_serialization = &R_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + let zero = <::Field as Field>::zero(); + let mut z_serialization = <::Field as Field>::serialize(&zero); + let z_bytes_len = z_serialization.as_ref().len(); - let one = <::Field as Field>::zero(); - let mut z_bytes = - Vec::from(<::Field as Field>::serialize(&one).as_ref()); + if bytes.len() != R_bytes_len + z_bytes_len { + return Err(Error::MalformedSignature); + } - let z_bytes_len = z_bytes.len(); + R_serialization + .as_mut() + .copy_from_slice(bytes.get(0..R_bytes_len).ok_or(Error::MalformedSignature)?); // We extract the exact length of bytes we expect, not just the remaining bytes with `bytes[R_bytes_len..]` - z_bytes[..].copy_from_slice( + z_serialization.as_mut().copy_from_slice( bytes - .as_ref() .get(R_bytes_len..R_bytes_len + z_bytes_len) .ok_or(Error::MalformedSignature)?, ); - let z_serialization = &z_bytes.try_into().map_err(|_| Error::MalformedSignature)?; - Ok(Self { - R: ::deserialize(R_serialization)?, - z: <::Field>::deserialize(z_serialization)?, + R: ::deserialize(&R_serialization)?, + z: <::Field>::deserialize(&z_serialization)?, }) } - /// Converts this signature to its [`Ciphersuite::SignatureSerialization`] in bytes. - pub fn serialize(&self) -> C::SignatureSerialization { - let mut bytes = vec![]; + /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. + pub fn deserialize(bytes: &[u8]) -> Result> { + C::deserialize_signature(bytes) + } + + /// Converts this signature to its default byte serialization. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn default_serialize(&self) -> Result, Error> { + let R_serialization = ::serialize(&self.R)?; + let z_serialization = <::Field>::serialize(&self.z); + + let R_bytes = R_serialization.as_ref(); + let z_bytes = z_serialization.as_ref(); + + let mut bytes = Vec::with_capacity(R_bytes.len() + z_bytes.len()); - bytes.extend(::serialize(&self.R).as_ref()); - bytes.extend(<::Field>::serialize(&self.z).as_ref()); + bytes.extend(R_bytes); + bytes.extend(z_bytes); - bytes.try_into().debugless_unwrap() + Ok(bytes) + } + + /// Converts this signature to its byte serialization. + pub fn serialize(&self) -> Result, Error> { + ::serialize_signature(self) } } @@ -92,7 +105,10 @@ where where S: serde::Serializer, { - serdect::slice::serialize_hex_lower_or_bin(&self.serialize().as_ref(), serializer) + serdect::slice::serialize_hex_lower_or_bin( + &self.serialize().map_err(serde::ser::Error::custom)?, + serializer, + ) } } @@ -108,19 +124,21 @@ where D: serde::Deserializer<'de>, { let bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?; - let array = bytes - .try_into() - .map_err(|_| serde::de::Error::custom("invalid byte length"))?; - let identifier = Signature::deserialize(array) + let signature = Signature::deserialize(&bytes) .map_err(|err| serde::de::Error::custom(format!("{err}")))?; - Ok(identifier) + Ok(signature) } } -impl std::fmt::Debug for Signature { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl core::fmt::Debug for Signature { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { f.debug_struct("Signature") - .field("R", &hex::encode(::serialize(&self.R).as_ref())) + .field( + "R", + &::serialize(&self.R) + .map(|s| hex::encode(s.as_ref())) + .unwrap_or("".to_string()), + ) .field( "z", &hex::encode(<::Field>::serialize(&self.z).as_ref()), diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs index 26d812f2f..20aa63408 100644 --- a/frost-core/src/signing_key.rs +++ b/frost-core/src/signing_key.rs @@ -1,11 +1,17 @@ //! Schnorr signature signing keys +use alloc::vec::Vec; + use rand_core::{CryptoRng, RngCore}; +use zeroize::ZeroizeOnDrop; -use crate::{random_nonzero, Ciphersuite, Error, Field, Group, Scalar, Signature, VerifyingKey}; +use crate::{ + random_nonzero, serialization::SerializableScalar, Challenge, Ciphersuite, Error, Field, Group, + Scalar, Signature, VerifyingKey, +}; /// A signing key for a Schnorr signature on a FROST [`Ciphersuite::Group`]. -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct SigningKey where C: Ciphersuite, @@ -25,43 +31,48 @@ where } /// Deserialize from bytes - pub fn deserialize( - bytes: <::Field as Field>::Serialization, - ) -> Result, Error> { - let scalar = - <::Field as Field>::deserialize(&bytes).map_err(Error::from)?; - - if scalar == <::Field as Field>::zero() { - return Err(Error::MalformedSigningKey); - } - - Ok(Self { scalar }) + pub fn deserialize(bytes: &[u8]) -> Result, Error> { + Self::from_scalar(SerializableScalar::deserialize(bytes)?.0) } /// Serialize `SigningKey` to bytes - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field as Field>::serialize(&self.scalar) + pub fn serialize(&self) -> Vec { + SerializableScalar::(self.scalar).serialize() } /// Create a signature `msg` using this `SigningKey`. - pub fn sign(&self, mut rng: R, msg: &[u8]) -> Signature { - let k = random_nonzero::(&mut rng); + pub fn sign(&self, rng: R, message: &[u8]) -> Signature { + ::single_sign(self, rng, message) + } + + /// Create a signature `msg` using this `SigningKey` using the default + /// signing. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn default_sign( + &self, + mut rng: R, + message: &[u8], + ) -> Signature { + let public = VerifyingKey::::from(self.clone()); - let R = ::generator() * k; + let (k, R) = ::generate_nonce(&mut rng); // Generate Schnorr challenge - let c = crate::challenge::(&R, &VerifyingKey::::from(*self), msg); + let c: Challenge = ::challenge(&R, &public, message).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); let z = k + (c.0 * self.scalar); Signature { R, z } } - /// Creates a SigningKey from a scalar. + /// Creates a SigningKey from a scalar. Returns an error if the scalar is zero. pub fn from_scalar( scalar: <<::Group as Group>::Field as Field>::Scalar, - ) -> Self { - Self { scalar } + ) -> Result> { + if scalar == <::Field as Field>::zero() { + return Err(Error::MalformedSigningKey); + } + Ok(Self { scalar }) } /// Return the underlying scalar. @@ -70,11 +81,22 @@ where } } -impl std::fmt::Debug for SigningKey +impl ZeroizeOnDrop for SigningKey where C: Ciphersuite {} + +impl Drop for SigningKey +where + C: Ciphersuite, +{ + fn drop(&mut self) { + self.scalar = <::Field as Field>::zero(); + } +} + +impl core::fmt::Debug for SigningKey where C: Ciphersuite, { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("SigningKey").field(&"").finish() } } @@ -84,9 +106,7 @@ where C: Ciphersuite, { fn from(signing_key: &SigningKey) -> Self { - VerifyingKey { - element: C::Group::generator() * signing_key.scalar, - } + VerifyingKey::new(C::Group::generator() * signing_key.scalar) } } diff --git a/frost-core/src/tests.rs b/frost-core/src/tests.rs index d2b1b7123..e4855c905 100644 --- a/frost-core/src/tests.rs +++ b/frost-core/src/tests.rs @@ -7,6 +7,7 @@ pub mod ciphersuite_generic; pub mod coefficient_commitment; pub mod helpers; pub mod proptests; +pub mod refresh; pub mod repairable; pub mod vectors; pub mod vectors_dkg; diff --git a/frost-core/src/tests/batch.rs b/frost-core/src/tests/batch.rs index 2f40433b1..008e0cb78 100644 --- a/frost-core/src/tests/batch.rs +++ b/frost-core/src/tests/batch.rs @@ -10,7 +10,7 @@ pub fn batch_verify(mut rng: R) { let msg = b"BatchVerifyTest"; let sig = sk.sign(&mut rng, &msg[..]); assert!(vk.verify(msg, &sig).is_ok()); - batch.queue((vk, sig, msg)); + batch.queue(batch::Item::::new(vk, sig, msg).unwrap()); } assert!(batch.verify(rng).is_ok()); } @@ -31,14 +31,14 @@ pub fn bad_batch_verify(mut rng: R) { } else { sk.sign(&mut rng, b"bad") }; - (vk, sig, msg).into() + batch::Item::::new(vk, sig, msg).unwrap() } 1 => { let sk = SigningKey::new(&mut rng); let vk = VerifyingKey::::from(&sk); let msg = b"BatchVerifyTest"; let sig = sk.sign(&mut rng, &msg[..]); - (vk, sig, msg).into() + batch::Item::::new(vk, sig, msg).unwrap() } _ => unreachable!(), }; diff --git a/frost-core/src/tests/ciphersuite_generic.rs b/frost-core/src/tests/ciphersuite_generic.rs index 4dd673176..57349f8f6 100644 --- a/frost-core/src/tests/ciphersuite_generic.rs +++ b/frost-core/src/tests/ciphersuite_generic.rs @@ -1,13 +1,19 @@ //! Ciphersuite-generic test functions. #![allow(clippy::type_complexity)] +#![cfg(feature = "serialization")] -use std::{collections::BTreeMap, convert::TryFrom}; +use alloc::{borrow::ToOwned, collections::BTreeMap, vec::Vec}; +use rand_core::{CryptoRng, RngCore}; use crate as frost; +use crate::keys::dkg::{round1, round2}; +use crate::keys::{SecretShare, SigningShare}; +use crate::round1::SigningNonces; +use crate::round2::SignatureShare; use crate::{ - keys::PublicKeyPackage, Error, Field, Group, Identifier, Signature, SigningKey, VerifyingKey, + keys::PublicKeyPackage, Error, Field, Group, Identifier, Signature, SigningKey, SigningPackage, + VerifyingKey, }; -use rand_core::{CryptoRng, RngCore}; use crate::Ciphersuite; @@ -15,13 +21,15 @@ use crate::Ciphersuite; pub fn check_zero_key_fails() { let zero = <<::Group as Group>::Field>::zero(); let encoded_zero = <<::Group as Group>::Field>::serialize(&zero); - let r = SigningKey::::deserialize(encoded_zero); + let r = SigningKey::::deserialize(encoded_zero.as_ref()); assert_eq!(r, Err(Error::MalformedSigningKey)); } /// Test share generation with a Ciphersuite pub fn check_share_generation(mut rng: R) { let secret = crate::SigningKey::::new(&mut rng); + // Simulate serialization / deserialization to ensure it works + let secret = SigningKey::deserialize(&secret.serialize()).unwrap(); let max_signers = 5; let min_signers = 3; @@ -47,9 +55,8 @@ pub fn check_share_generation(mut rng: R assert_eq!( frost::keys::reconstruct::(&key_packages) .unwrap() - .serialize() - .as_ref(), - secret.serialize().as_ref() + .serialize(), + secret.serialize() ); // Test error cases @@ -107,29 +114,36 @@ pub fn check_sign_with_dealer( let max_signers = 5; let min_signers = 3; - let (shares, pubkeys) = frost::keys::generate_with_dealer( + let (shares, pub_key_package) = frost::keys::generate_with_dealer( max_signers, min_signers, frost::keys::IdentifierList::Default, &mut rng, ) .unwrap(); + // Simulate serialization / deserialization to ensure it works + let pub_key_package = + PublicKeyPackage::deserialize(&pub_key_package.serialize().unwrap()).unwrap(); // Verifies the secret shares from the dealer let mut key_packages: BTreeMap, frost::keys::KeyPackage> = BTreeMap::new(); for (k, v) in shares { + // Simulate serialization / deserialization to ensure it works + let v = SecretShare::::deserialize(&v.serialize().unwrap()).unwrap(); let key_package = frost::keys::KeyPackage::try_from(v).unwrap(); + // Simulate serialization / deserialization to ensure it works + let key_package = + frost::keys::KeyPackage::deserialize(&key_package.serialize().unwrap()).unwrap(); key_packages.insert(k, key_package); } - // Check if it fails with not enough signers. Usually this would return an - // error before even running the signing procedure, because `KeyPackage` - // contains the correct `min_signers` value and the signing procedure checks - // if the number of shares is at least `min_signers`. To bypass the check - // and test if the protocol itself fails with not enough signers, we modify - // the `KeyPackages`s, decrementing their saved `min_signers` value before - // running the signing procedure. + // Check if it fails with not enough signers. Both the KeyPackages and + // the PublicKeyPackage have their min_signers decremented so that the + // early validation check in aggregate() passes, and we can verify that + // the cryptographic aggregation itself fails when too few shares are used. + let mut pub_key_package_insufficient = pub_key_package.clone(); + pub_key_package_insufficient.min_signers = Some(min_signers - 1); let r = check_sign( min_signers - 1, key_packages @@ -143,11 +157,11 @@ pub fn check_sign_with_dealer( }) .collect(), &mut rng, - pubkeys.clone(), + pub_key_package_insufficient, ); assert_eq!(r, Err(Error::InvalidSignature)); - check_sign(min_signers, key_packages, rng, pubkeys).unwrap() + check_sign(min_signers, key_packages, rng, pub_key_package).unwrap() } /// Test FROST signing with trusted dealer fails with invalid numbers of signers. @@ -202,7 +216,10 @@ pub fn check_sign( // Round 1: generating nonces and signing commitments for each participant //////////////////////////////////////////////////////////////////////////// - for participant_identifier in key_packages.keys().take(min_signers as usize).cloned() { + for participant_identifier in key_packages.keys().take(min_signers as usize) { + // Simulate serialization / deserialization to ensure it works + let participant_identifier = + Identifier::deserialize(&participant_identifier.serialize()).unwrap(); // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _min_signers_. let (nonces, commitments) = frost::round1::commit( @@ -212,6 +229,11 @@ pub fn check_sign( .signing_share(), &mut rng, ); + // Simulate serialization / deserialization to ensure it works + let nonces = SigningNonces::deserialize(&nonces.serialize().unwrap()).unwrap(); + let commitments = + frost::round1::SigningCommitments::deserialize(&commitments.serialize().unwrap()) + .unwrap(); nonces_map.insert(participant_identifier, nonces); commitments_map.insert(participant_identifier, commitments); } @@ -221,7 +243,10 @@ pub fn check_sign( // - take one (unused) commitment per signing participant let mut signature_shares = BTreeMap::new(); let message = "message to sign".as_bytes(); - let signing_package = frost::SigningPackage::new(commitments_map, message); + let signing_package = SigningPackage::new(commitments_map, message); + // Simulate serialization / deserialization to ensure it works + let signing_package = + SigningPackage::deserialize(&signing_package.serialize().unwrap()).unwrap(); //////////////////////////////////////////////////////////////////////////// // Round 2: each participant generates their signature share @@ -240,6 +265,8 @@ pub fn check_sign( // Each participant generates their signature share. let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + // Simulate serialization / deserialization to ensure it works + let signature_share = SignatureShare::deserialize(&signature_share.serialize()).unwrap(); signature_shares.insert(*participant_identifier, signature_share); } @@ -248,21 +275,24 @@ pub fn check_sign( // generates the final signature. //////////////////////////////////////////////////////////////////////////// - #[cfg(not(feature = "cheater-detection"))] - let pubkey_package = PublicKeyPackage { - header: pubkey_package.header, - verifying_shares: BTreeMap::new(), - verifying_key: pubkey_package.verifying_key, - }; - check_aggregate_errors( signing_package.clone(), signature_shares.clone(), pubkey_package.clone(), ); + check_verifying_shares( + pubkey_package.clone(), + signing_package.clone(), + signature_shares.clone(), + ); + + check_verify_signature_share(&pubkey_package, &signing_package, &signature_shares); + // Aggregate (also verifies the signature shares) let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?; + // Simulate serialization / deserialization to ensure it works + let group_signature = Signature::deserialize(&group_signature.serialize().unwrap()).unwrap(); // Check that the threshold signature can be verified by the group public // key (the verification key). @@ -312,7 +342,6 @@ fn check_aggregate_errors( signature_shares: BTreeMap, frost::round2::SignatureShare>, pubkey_package: frost::keys::PublicKeyPackage, ) { - #[cfg(feature = "cheater-detection")] check_aggregate_corrupted_share( signing_package.clone(), signature_shares.clone(), @@ -331,13 +360,65 @@ fn check_aggregate_corrupted_share( mut signature_shares: BTreeMap, frost::round2::SignatureShare>, pubkey_package: frost::keys::PublicKeyPackage, ) { + use crate::{round2::SignatureShare, CheaterDetection}; + let one = <::Group as Group>::Field::one(); - // Corrupt a share - let id = *signature_shares.keys().next().unwrap(); - signature_shares.get_mut(&id).unwrap().share = signature_shares[&id].share + one; + // Corrupt two shares + let id1 = *signature_shares.keys().next().unwrap(); + *signature_shares.get_mut(&id1).unwrap() = + SignatureShare::new(signature_shares[&id1].to_scalar() + one); + let id2 = *signature_shares.keys().nth(1).unwrap(); + *signature_shares.get_mut(&id2).unwrap() = + SignatureShare::new(signature_shares[&id2].to_scalar() + one); + let e = frost::aggregate(&signing_package, &signature_shares, &pubkey_package).unwrap_err(); - assert_eq!(e.culprit(), Some(id)); - assert_eq!(e, Error::InvalidSignatureShare { culprit: id }); + assert_eq!(e.culprits(), vec![id1]); + assert_eq!( + e, + Error::InvalidSignatureShare { + culprits: vec![id1] + } + ); + + let e = frost::aggregate_custom( + &signing_package, + &signature_shares, + &pubkey_package, + crate::CheaterDetection::Disabled, + ) + .unwrap_err(); + assert_eq!(e.culprits(), vec![]); + assert_eq!(e, Error::InvalidSignature); + + let e = frost::aggregate_custom( + &signing_package, + &signature_shares, + &pubkey_package, + crate::CheaterDetection::FirstCheater, + ) + .unwrap_err(); + assert_eq!(e.culprits(), vec![id1]); + assert_eq!( + e, + Error::InvalidSignatureShare { + culprits: vec![id1] + } + ); + + let e = frost::aggregate_custom( + &signing_package, + &signature_shares, + &pubkey_package, + CheaterDetection::AllCheaters, + ) + .unwrap_err(); + assert_eq!(e.culprits(), vec![id1, id2]); + assert_eq!( + e, + Error::InvalidSignatureShare { + culprits: vec![id1, id2] + } + ); } /// Test NCC-E008263-4VP audit finding (PublicKeyPackage). @@ -365,7 +446,7 @@ pub fn check_sign_with_dkg( mut rng: R, ) -> (Vec, Signature, VerifyingKey) where - C::Group: std::cmp::PartialEq, + C::Group: core::cmp::PartialEq, { //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 @@ -398,9 +479,24 @@ where frost::keys::dkg::part1(participant_identifier, max_signers, min_signers, &mut rng) .unwrap(); + // Simulate serialization / deserialization to ensure it works + let round1_secret_package = frost::keys::dkg::round1::SecretPackage::::deserialize( + &round1_secret_package.serialize().unwrap(), + ) + .unwrap(); + let round1_package = frost::keys::dkg::round1::Package::::deserialize( + &round1_package.serialize().unwrap(), + ) + .unwrap(); + // Store the participant's secret package for later use. // In practice each participant will store it in their own environment. - round1_secret_packages.insert(participant_identifier, round1_secret_package); + round1_secret_packages.insert( + participant_identifier, + // Serialization roundtrip to simulate storage for later + round1::SecretPackage::deserialize(&round1_secret_package.serialize().unwrap()) + .unwrap(), + ); // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be @@ -415,7 +511,11 @@ where received_round1_packages .entry(receiver_participant_identifier) .or_default() - .insert(participant_identifier, round1_package.clone()); + .insert( + participant_identifier, + // Serialization roundtrip to simulate communication + round1::Package::deserialize(&round1_package.serialize().unwrap()).unwrap(), + ); } } @@ -445,9 +545,20 @@ where let (round2_secret_package, round2_packages) = frost::keys::dkg::part2(round1_secret_package, round1_packages).expect("should work"); + // Simulate serialization / deserialization to ensure it works + let round2_secret_package = frost::keys::dkg::round2::SecretPackage::::deserialize( + &round2_secret_package.serialize().unwrap(), + ) + .unwrap(); + // Store the participant's secret package for later use. // In practice each participant will store it in their own environment. - round2_secret_packages.insert(participant_identifier, round2_secret_package); + round2_secret_packages.insert( + participant_identifier, + // Serialization roundtrip to simulate storage for later + round2::SecretPackage::deserialize(&round2_secret_package.serialize().unwrap()) + .unwrap(), + ); // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be @@ -455,10 +566,19 @@ where // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { + // Simulate serialization / deserialization to ensure it works + let round2_package = frost::keys::dkg::round2::Package::::deserialize( + &round2_package.serialize().unwrap(), + ) + .unwrap(); received_round2_packages .entry(receiver_identifier) .or_insert_with(BTreeMap::new) - .insert(participant_identifier, round2_package); + .insert( + participant_identifier, + // Serialization roundtrip to simulate communication + round2::Package::deserialize(&round2_package.serialize().unwrap()).unwrap(), + ); } } @@ -471,9 +591,9 @@ where // will have all the participant's packages. let mut key_packages = BTreeMap::new(); - // Map of the verifying key of each participant. + // Map of the verifying share of each participant. // Used by the signing test that follows. - let mut verifying_keys = BTreeMap::new(); + let mut verifying_shares = BTreeMap::new(); // The group public key, used by the signing test that follows. let mut verifying_key = None; // For each participant, store the set of verifying keys they have computed. @@ -484,7 +604,7 @@ where // for each signature before being aggregated. let mut pubkey_packages_by_participant = BTreeMap::new(); - check_part3_different_participants( + check_part3_errors( max_signers, round2_secret_packages.clone(), received_round1_packages.clone(), @@ -501,7 +621,14 @@ where &received_round2_packages[&participant_identifier], ) .unwrap(); - verifying_keys.insert(participant_identifier, key_package.verifying_share); + // Simulate serialization / deserialization to ensure it works + let key_package = + frost::keys::KeyPackage::deserialize(&key_package.serialize().unwrap()).unwrap(); + let pubkey_package_for_participant = frost::keys::PublicKeyPackage::deserialize( + &pubkey_package_for_participant.serialize().unwrap(), + ) + .unwrap(); + verifying_shares.insert(participant_identifier, key_package.verifying_share); // Test if all verifying_key are equal if let Some(previous_verifying_key) = verifying_key { assert_eq!(previous_verifying_key, key_package.verifying_key) @@ -514,17 +641,51 @@ where // Test if the set of verifying keys is correct for all participants. for verifying_keys_for_participant in pubkey_packages_by_participant.values() { - assert!(verifying_keys_for_participant.verifying_shares == verifying_keys); + assert!(verifying_keys_for_participant.verifying_shares == verifying_shares); } - let pubkeys = frost::keys::PublicKeyPackage::new(verifying_keys, verifying_key.unwrap()); + let pubkeys = pubkey_packages_by_participant + .first_key_value() + .unwrap() + .1 + .clone(); + // Simulate serialization / deserialization to ensure it works + let pubkeys = + frost::keys::PublicKeyPackage::deserialize(&pubkeys.serialize().unwrap()).unwrap(); // Proceed with the signing test. check_sign(min_signers, key_packages, rng, pubkeys).unwrap() } +/// Check for error cases related to DKG part3. +fn check_part3_errors( + max_signers: u16, + round2_secret_packages: BTreeMap, frost::keys::dkg::round2::SecretPackage>, + received_round1_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + >, + received_round2_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round2::Package>, + >, +) { + check_part3_different_participants( + max_signers, + round2_secret_packages.clone(), + received_round1_packages.clone(), + received_round2_packages.clone(), + ); + check_part3_corrupted_share( + max_signers, + round2_secret_packages, + received_round1_packages, + received_round2_packages, + ); +} + /// Check that calling dkg::part3() with distinct sets of participants fail. -fn check_part3_different_participants( +pub fn check_part3_different_participants( max_signers: u16, round2_secret_packages: BTreeMap, frost::keys::dkg::round2::SecretPackage>, received_round1_packages: BTreeMap< @@ -560,6 +721,49 @@ fn check_part3_different_participants( } } +/// Check that calling dkg::part3() with a corrupted share fail, and the +/// culprit is correctly identified. +fn check_part3_corrupted_share( + max_signers: u16, + round2_secret_packages: BTreeMap, frost::keys::dkg::round2::SecretPackage>, + received_round1_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + >, + received_round2_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round2::Package>, + >, +) { + // For each participant, perform the third part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + + // Remove the first package from the map, and reinsert it with an unrelated + // Do the same for Round 2 packages + let mut received_round2_packages = + received_round2_packages[&participant_identifier].clone(); + let culprit = *received_round2_packages.keys().next().unwrap(); + let package = received_round2_packages.get_mut(&culprit).unwrap(); + let one = <::Group as Group>::Field::one(); + package.signing_share = SigningShare::new(package.signing_share().to_scalar() + one); + + let r = frost::keys::dkg::part3( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages, + ) + .expect_err("Should have failed due to corrupted share"); + assert_eq!( + r, + Error::InvalidSecretShare { + culprit: Some(culprit) + } + ) + } +} + /// Test FROST signing with trusted dealer with a Ciphersuite, using specified /// Identifiers. pub fn check_sign_with_dealer_and_identifiers( @@ -633,17 +837,19 @@ pub fn check_sign_with_dealer_and_identifiers( round1_secret_package: frost::keys::dkg::round1::SecretPackage, mut round1_packages: BTreeMap, frost::keys::dkg::round1::Package>, ) { + // Check if a corrupted proof of knowledge results in failure. let one = <::Group as Group>::Field::one(); // Corrupt a PoK let id = *round1_packages.keys().next().unwrap(); round1_packages.get_mut(&id).unwrap().proof_of_knowledge.z = round1_packages[&id].proof_of_knowledge.z + one; let e = frost::keys::dkg::part2(round1_secret_package, &round1_packages).unwrap_err(); - assert_eq!(e.culprit(), Some(id)); + assert_eq!(e.culprits(), vec![id]); assert_eq!(e, Error::InvalidProofOfKnowledge { culprit: id }); } @@ -652,17 +858,17 @@ pub fn check_error_culprit() { let identifier: frost::Identifier = 42u16.try_into().unwrap(); let e = Error::InvalidSignatureShare { - culprit: identifier, + culprits: vec![identifier], }; - assert_eq!(e.culprit(), Some(identifier)); + assert_eq!(e.culprits(), vec![identifier]); let e = Error::InvalidProofOfKnowledge { culprit: identifier, }; - assert_eq!(e.culprit(), Some(identifier)); + assert_eq!(e.culprits(), vec![identifier]); let e: Error = Error::InvalidSignature; - assert_eq!(e.culprit(), None); + assert_eq!(e.culprits(), vec![]); } /// Test identifier derivation with a Ciphersuite @@ -740,7 +946,7 @@ pub fn check_sign_with_missing_identifier( + pubkeys: PublicKeyPackage, + signing_package: SigningPackage, + mut signature_shares: BTreeMap, SignatureShare>, +) { + let one = <::Group as Group>::Field::one(); + + // Corrupt last share + let id = *signature_shares.keys().last().unwrap(); + *signature_shares.get_mut(&id).unwrap() = + SignatureShare::new(signature_shares[&id].to_scalar() + one); + + let e = frost::aggregate(&signing_package, &signature_shares, &pubkeys).unwrap_err(); + assert_eq!(e.culprits(), vec![id]); + assert_eq!(e, Error::InvalidSignatureShare { culprits: vec![id] }); +} + +// Checks if `verify_signature_share()` works correctly. +fn check_verify_signature_share( + pubkeys: &PublicKeyPackage, + signing_package: &SigningPackage, + signature_shares: &BTreeMap, SignatureShare>, +) { + for (identifier, signature_share) in signature_shares { + frost::verify_signature_share( + *identifier, + pubkeys.verifying_shares().get(identifier).unwrap(), + signature_share, + signing_package, + pubkeys.verifying_key(), + ) + .expect("should pass"); + } + + for (identifier, signature_share) in signature_shares { + let one = <::Group as Group>::Field::one(); + // Corrupt share + let signature_share = SignatureShare::new(signature_share.to_scalar() + one); + + frost::verify_signature_share( + *identifier, + pubkeys.verifying_shares().get(identifier).unwrap(), + &signature_share, + signing_package, + pubkeys.verifying_key(), + ) + .expect_err("should have failed"); + } +} + +/// Test FROST signing in an async context. +/// The ultimate goal of the test is to ensure that types are Send + Sync. +pub async fn async_check_sign( + mut rng: R, +) { + tokio::spawn(async move { + let max_signers = 5; + let min_signers = 3; + let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + // The test is sprinkled with await points to ensure that types that + // cross them are Send + Sync. + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + + // Verifies the secret shares from the dealer + let key_packages: BTreeMap, frost::keys::KeyPackage> = shares + .into_iter() + .map(|(k, v)| (k, frost::keys::KeyPackage::try_from(v).unwrap())) + .collect(); + + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + + let mut nonces_map: BTreeMap, frost::round1::SigningNonces> = + BTreeMap::new(); + let mut commitments_map: BTreeMap< + frost::Identifier, + frost::round1::SigningCommitments, + > = BTreeMap::new(); + + for participant_identifier in key_packages.keys().take(min_signers as usize).cloned() { + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _min_signers_. + let (nonces, commitments) = frost::round1::commit( + key_packages + .get(&participant_identifier) + .unwrap() + .signing_share(), + &mut rng, + ); + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); + } + + let mut signature_shares = BTreeMap::new(); + let message = "message to sign".as_bytes(); + let signing_package = SigningPackage::new(commitments_map, message); + + for participant_identifier in nonces_map.keys() { + let key_package = key_packages.get(participant_identifier).unwrap(); + let nonces_to_use = nonces_map.get(participant_identifier).unwrap(); + let signature_share = + frost::round2::sign(&signing_package, nonces_to_use, key_package).unwrap(); + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + signature_shares.insert(*participant_identifier, signature_share); + } + + let group_signature = + frost::aggregate(&signing_package, &signature_shares, &pubkey_package).unwrap(); + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + + pubkey_package + .verifying_key + .verify(message, &group_signature) + .unwrap(); + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + + for (participant_identifier, _) in nonces_map.clone() { + let key_package = key_packages.get(&participant_identifier).unwrap(); + key_package + .verifying_key + .verify(message, &group_signature) + .unwrap(); + tokio::time::sleep(core::time::Duration::from_millis(1)).await; + } + }) + .await + .unwrap(); +} diff --git a/frost-core/src/tests/coefficient_commitment.rs b/frost-core/src/tests/coefficient_commitment.rs index 36c241c8c..b491108b9 100644 --- a/frost-core/src/tests/coefficient_commitment.rs +++ b/frost-core/src/tests/coefficient_commitment.rs @@ -1,10 +1,8 @@ //! CoefficientCommitment functions -use std::convert::TryFrom; - use crate as frost; use crate::{keys::CoefficientCommitment, tests::helpers::generate_element, Group}; -use debugless_unwrap::DebuglessUnwrap; +use debugless_unwrap::DebuglessUnwrapExt; use rand_core::{CryptoRng, RngCore}; use serde_json::Value; @@ -16,23 +14,25 @@ pub fn check_serialization_of_coefficient_commitment(&mut rng); - let expected = ::serialize(&element); + let expected = ::serialize(&element).unwrap(); - let data = frost::keys::CoefficientCommitment::(element).serialize(); + let data = frost::keys::CoefficientCommitment::::new(element) + .serialize() + .unwrap(); - assert!(expected.as_ref() == data.as_ref()); + assert!(expected.as_ref() == data); } /// Test create a CoefficientCommitment. pub fn check_create_coefficient_commitment(mut rng: R) { let element = generate_element::(&mut rng); - let expected = CoefficientCommitment::(element); + let expected = CoefficientCommitment::::new(element); - let serialized_element = ::serialize(&element); + let serialized_element = ::serialize(&element).unwrap(); let coeff_commitment = - frost::keys::CoefficientCommitment::::deserialize(serialized_element).unwrap(); + frost::keys::CoefficientCommitment::::deserialize(serialized_element.as_ref()).unwrap(); assert!(expected == coeff_commitment); } @@ -44,11 +44,12 @@ pub fn check_create_coefficient_commitment_error( let values = &commitment_helpers["elements"]; let serialized: ::Serialization = ::Serialization::try_from( - hex::decode(values["invalid_element"].as_str().unwrap()).unwrap(), + &hex::decode(values["invalid_element"].as_str().unwrap()).unwrap(), ) .debugless_unwrap(); - let coeff_commitment = frost::keys::CoefficientCommitment::::deserialize(serialized); + let coeff_commitment = + frost::keys::CoefficientCommitment::::deserialize(serialized.as_ref()); assert!(coeff_commitment.is_err()); } @@ -59,7 +60,7 @@ pub fn check_get_value_of_coefficient_commitment(&mut rng); - let coeff_commitment = frost::keys::CoefficientCommitment::(element); + let coeff_commitment = frost::keys::CoefficientCommitment::::new(element); let value = coeff_commitment.value(); assert!(value == element) diff --git a/frost-core/src/tests/proptests.rs b/frost-core/src/tests/proptests.rs index f7c70897e..9b0fbcb9f 100644 --- a/frost-core/src/tests/proptests.rs +++ b/frost-core/src/tests/proptests.rs @@ -59,12 +59,12 @@ where // The signature data is stored in (refined) byte types, but do a round trip // conversion to raw bytes to exercise those code paths. let _sig = { - let bytes = self.sig.serialize(); - Signature::::deserialize(bytes) + let bytes = self.sig.serialize().unwrap(); + Signature::::deserialize(&bytes) }; // Check that the verification key is a valid key. - let _pub_key = VerifyingKey::::deserialize(self.vk.serialize()) + let _pub_key = VerifyingKey::::deserialize(&self.vk.serialize().unwrap()) .expect("The test verification key to be well-formed."); // Check that signature validation has the expected result. diff --git a/frost-core/src/tests/refresh.rs b/frost-core/src/tests/refresh.rs new file mode 100644 index 000000000..8f2d56f6c --- /dev/null +++ b/frost-core/src/tests/refresh.rs @@ -0,0 +1,610 @@ +//! Test for Refreshing shares +#![cfg(feature = "serialization")] + +use rand_core::{CryptoRng, RngCore}; + +use crate::keys::dkg::{round1, round2}; +use crate::keys::generate_with_dealer; +use crate::keys::refresh::{ + compute_refreshing_shares, refresh_dkg_part1, refresh_dkg_part2, refresh_share, +}; +use crate::{self as frost}; +use crate::{ + keys::{KeyPackage, PublicKeyPackage, SecretShare}, + Ciphersuite, Error, Identifier, Signature, VerifyingKey, +}; + +use crate::tests::ciphersuite_generic::check_part3_different_participants; + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use super::ciphersuite_generic::check_sign; + +/// We want to test that recovered share matches the original share +pub fn check_refresh_shares_with_dealer(mut rng: R) { + // Compute shares + + //////////////////////////////////////////////////////////////////////////// + // Old Key generation + //////////////////////////////////////////////////////////////////////////// + + const MAX_SIGNERS: u16 = 5; + const MIN_SIGNERS: u16 = 3; + let (old_shares, pub_key_package) = generate_with_dealer( + MAX_SIGNERS, + MIN_SIGNERS, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + let mut old_key_packages: BTreeMap, KeyPackage> = BTreeMap::new(); + + for (k, v) in old_shares { + let key_package = KeyPackage::try_from(v).unwrap(); + old_key_packages.insert(k, key_package); + } + + //////////////////////////////////////////////////////////////////////////// + // New Key generation + //////////////////////////////////////////////////////////////////////////// + + // Signer 2 will be removed and Signers 1, 3, 4 & 5 will remain + + let remaining_ids = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + + // Trusted Dealer generates zero keys and new public key package + + let (zero_shares, new_pub_key_package) = + compute_refreshing_shares(pub_key_package, &remaining_ids, &mut rng).unwrap(); + // Simulate serialization / deserialization to ensure it works + let new_pub_key_package = + frost::keys::PublicKeyPackage::deserialize(&new_pub_key_package.serialize().unwrap()) + .unwrap(); + + // Each participant refreshes their share + + let mut new_shares = BTreeMap::new(); + + for i in 0..remaining_ids.len() { + let identifier = remaining_ids[i]; + let current_share = &old_key_packages[&identifier]; + // Do a serialization roundtrip to simulate real usage + let zero_share = SecretShare::deserialize(&zero_shares[i].serialize().unwrap()).unwrap(); + let new_share = refresh_share(zero_share, current_share).unwrap(); + new_shares.insert(identifier, new_share); + } + + let mut key_packages: BTreeMap, KeyPackage> = BTreeMap::new(); + + for (k, v) in new_shares { + // Simulate serialization / deserialization to ensure it works + let v = KeyPackage::::deserialize(&v.serialize().unwrap()).unwrap(); + key_packages.insert(k, v); + } + check_sign(MIN_SIGNERS, key_packages, rng, new_pub_key_package).unwrap(); +} + +/// We want to check that shares are refreshed with valid signers +pub fn check_refresh_shares_with_dealer_fails_with_invalid_signers< + C: Ciphersuite, + R: RngCore + CryptoRng, +>( + identifiers: &[Identifier], + error: Error, + mut rng: R, +) { + let (_old_shares, pub_key_package) = + generate_with_dealer::(5, 2, frost::keys::IdentifierList::Default, &mut rng).unwrap(); + let out = compute_refreshing_shares(pub_key_package, identifiers, &mut rng); + + assert!(out.is_err()); + assert!(out == Err(error)) +} + +/// We want to test that refresh share fails if the identifiers don't match the +/// identifiers in the public key package +pub fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package< + C: Ciphersuite, + R: RngCore + CryptoRng, +>( + mut rng: R, +) { + // Compute shares + + //////////////////////////////////////////////////////////////////////////// + // Old Key generation + //////////////////////////////////////////////////////////////////////////// + + const MAX_SIGNERS: u16 = 3; + const MIN_SIGNERS: u16 = 2; + let (old_shares, incorrect_pub_key_package) = generate_with_dealer( + MAX_SIGNERS, + MIN_SIGNERS, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + let mut old_key_packages: BTreeMap, KeyPackage> = BTreeMap::new(); + + for (k, v) in old_shares { + let key_package = KeyPackage::try_from(v).unwrap(); + old_key_packages.insert(k, key_package); + } + + //////////////////////////////////////////////////////////////////////////// + // New Key generation + //////////////////////////////////////////////////////////////////////////// + + // Signer 2 will be removed and Signers 1, 3, 4 & 5 will remain + + let remaining_ids = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + + // Trusted Dealer generates zero keys and new public key package + + let e = + compute_refreshing_shares(incorrect_pub_key_package, &remaining_ids, &mut rng).unwrap_err(); + + assert_eq!(e, Error::UnknownIdentifier) +} + +/// Check serialisation +#[cfg(feature = "serialization")] +pub fn check_refresh_shares_with_dealer_serialisation( + mut rng: R, +) { + //////////////////////////////////////////////////////////////////////////// + // Old Key generation + //////////////////////////////////////////////////////////////////////////// + + const MAX_SIGNERS: u16 = 5; + const MIN_SIGNERS: u16 = 3; + let (_old_shares, pub_key_package) = generate_with_dealer( + MAX_SIGNERS, + MIN_SIGNERS, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + //////////////////////////////////////////////////////////////////////////// + // New Key generation + // + // Zero key is calculated by trusted dealer + // Participant 2 will be removed and Participants 1, 3, 4 & 5 will remain + //////////////////////////////////////////////////////////////////////////// + + let remaining_ids = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + + let (zero_shares, new_pub_key_package) = + compute_refreshing_shares(pub_key_package, &remaining_ids, &mut rng).unwrap(); + + // Trusted dealer serialises zero shares and key package + + let zero_shares_serialised = SecretShare::::serialize(&zero_shares[0]); + + assert!(zero_shares_serialised.is_ok()); + + let new_pub_key_package_serialised = PublicKeyPackage::::serialize(&new_pub_key_package); + + assert!(new_pub_key_package_serialised.is_ok()); + + // Participant 1 deserialises zero share and key package + + let zero_share = SecretShare::::deserialize(&zero_shares_serialised.unwrap()); + + assert!(zero_share.is_ok()); + + let new_pub_key_package = + PublicKeyPackage::::deserialize(&new_pub_key_package_serialised.unwrap()); + + assert!(new_pub_key_package.is_ok()); + + // Participant 1 checks Key Package can be created from Secret Share + + let key_package = KeyPackage::::try_from(zero_share.unwrap()); + + assert!(key_package.is_ok()); +} + +/// Test FROST signing with DKG with a Ciphersuite. +pub fn check_refresh_shares_with_dkg( + mut rng: R, +) -> (Vec, Signature, VerifyingKey) +where + C::Group: core::cmp::PartialEq, +{ + //////////////////////////////////////////////////////////////////////////// + // Old Key generation + //////////////////////////////////////////////////////////////////////////// + + let old_max_signers = 5; + let min_signers = 3; + let (old_shares, pub_key_package) = generate_with_dealer( + old_max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + let mut old_key_packages: BTreeMap, KeyPackage> = BTreeMap::new(); + + for (k, v) in old_shares { + let key_package = KeyPackage::try_from(v).unwrap(); + old_key_packages.insert(k, key_package); + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, Round 1 + //////////////////////////////////////////////////////////////////////////// + + let max_signers = 4; + let min_signers = 3; + + let remaining_ids = vec![ + Identifier::try_from(4).unwrap(), + Identifier::try_from(2).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(1).unwrap(), + ]; + + // Keep track of each participant's round 1 secret package. + // In practice each participant will keep its copy; no one + // will have all the participant's packages. + let mut round1_secret_packages: BTreeMap< + frost::Identifier, + frost::keys::dkg::round1::SecretPackage, + > = BTreeMap::new(); + + // Keep track of all round 1 packages sent to the given participant. + // This is used to simulate the broadcast; in practice the packages + // will be sent through some communication channel. + let mut received_round1_packages: BTreeMap< + frost::Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + > = BTreeMap::new(); + + // For each participant, perform the first part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_identifier in remaining_ids.clone() { + let (round1_secret_package, round1_package) = + refresh_dkg_part1(participant_identifier, max_signers, min_signers, &mut rng).unwrap(); + + // Simulate serialization / deserialization to ensure it works + let round1_secret_package = frost::keys::dkg::round1::SecretPackage::::deserialize( + &round1_secret_package.serialize().unwrap(), + ) + .unwrap(); + let round1_package = frost::keys::dkg::round1::Package::::deserialize( + &round1_package.serialize().unwrap(), + ) + .unwrap(); + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round1_secret_packages.insert( + participant_identifier, + // Serialization roundtrip to simulate storage for later + round1::SecretPackage::deserialize(&round1_secret_package.serialize().unwrap()) + .unwrap(), + ); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + for receiver_participant_identifier in remaining_ids.clone() { + if receiver_participant_identifier == participant_identifier { + continue; + } + received_round1_packages + .entry(receiver_participant_identifier) + .or_default() + .insert( + participant_identifier, + // Serialization roundtrip to simulate communication + round1::Package::deserialize(&round1_package.serialize().unwrap()).unwrap(), + ); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, Round 2 + //////////////////////////////////////////////////////////////////////////// + // Keep track of each participant's round 2 secret package. + // In practice each participant will keep its copy; no one + // will have all the participant's packages. + let mut round2_secret_packages = BTreeMap::new(); + + // Keep track of all round 2 packages sent to the given participant. + // This is used to simulate the broadcast; in practice the packages + // will be sent through some communication channel. + let mut received_round2_packages = BTreeMap::new(); + + // For each participant, perform the second part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_identifier in remaining_ids.clone() { + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + let (round2_secret_package, round2_packages) = + refresh_dkg_part2(round1_secret_package, round1_packages).expect("should work"); + + // Simulate serialization / deserialization to ensure it works + let round2_secret_package = frost::keys::dkg::round2::SecretPackage::::deserialize( + &round2_secret_package.serialize().unwrap(), + ) + .unwrap(); + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round2_secret_packages.insert( + participant_identifier, + // Serialization roundtrip to simulate storage for later + round2::SecretPackage::deserialize(&round2_secret_package.serialize().unwrap()) + .unwrap(), + ); + + // "Send" the round 2 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + // Note that, in contrast to the previous part, here each other participant + // gets its own specific package. + for (receiver_identifier, round2_package) in round2_packages { + // Simulate serialization / deserialization to ensure it works + let round2_package = frost::keys::dkg::round2::Package::::deserialize( + &round2_package.serialize().unwrap(), + ) + .unwrap(); + received_round2_packages + .entry(receiver_identifier) + .or_insert_with(BTreeMap::new) + .insert( + participant_identifier, + // Serialization roundtrip to simulate communication + round2::Package::deserialize(&round2_package.serialize().unwrap()).unwrap(), + ); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, final computation + //////////////////////////////////////////////////////////////////////////// + + // Keep track of each participant's long-lived key package. + // In practice each participant will keep its copy; no one + // will have all the participant's packages. + let mut key_packages = BTreeMap::new(); + + // Map of the verifying share of each participant. + // Used by the signing test that follows. + let mut verifying_shares = BTreeMap::new(); + // The group public key, used by the signing test that follows. + let mut verifying_key = None; + // For each participant, store the set of verifying keys they have computed. + // This is used to check if the set is correct (the same) for all participants. + // In practice, if there is a Coordinator, only they need to store the set. + // If there is not, then all candidates must store their own sets. + // The verifying keys are used to verify the signature shares produced + // for each signature before being aggregated. + let mut pubkey_packages_by_participant = BTreeMap::new(); + + check_part3_different_participants( + max_signers, + round2_secret_packages.clone(), + received_round1_packages.clone(), + received_round2_packages.clone(), + ); + + // For each participant, this is where they refresh their shares + // In practice, each participant will perform this on their own environments. + for participant_identifier in remaining_ids.clone() { + let (key_package, pubkey_package_for_participant) = + frost::keys::refresh::refresh_dkg_shares( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages[&participant_identifier], + pub_key_package.clone(), + old_key_packages[&participant_identifier].clone(), + ) + .unwrap(); + // Simulate serialization / deserialization to ensure it works + let key_package = KeyPackage::deserialize(&key_package.serialize().unwrap()).unwrap(); + let pubkey_package_for_participant = frost::keys::PublicKeyPackage::deserialize( + &pubkey_package_for_participant.serialize().unwrap(), + ) + .unwrap(); + verifying_shares.insert(participant_identifier, key_package.verifying_share); + // Test if all verifying_key are equal + if let Some(previous_verifying_key) = verifying_key { + assert_eq!(previous_verifying_key, key_package.verifying_key) + } + verifying_key = Some(key_package.verifying_key); + key_packages.insert(participant_identifier, key_package); + pubkey_packages_by_participant + .insert(participant_identifier, pubkey_package_for_participant); + } + + // Test if the set of verifying keys is correct for all participants. + for verifying_keys_for_participant in pubkey_packages_by_participant.values() { + assert!(verifying_keys_for_participant.verifying_shares == verifying_shares); + } + + let pubkeys = pubkey_packages_by_participant + .first_key_value() + .unwrap() + .1 + .clone(); + // Simulate serialization / deserialization to ensure it works + let pubkeys = + frost::keys::PublicKeyPackage::deserialize(&pubkeys.serialize().unwrap()).unwrap(); + + // Proceed with the signing test. + check_sign(min_signers, key_packages, rng, pubkeys).unwrap() +} + +/// Test FROST signing with DKG with a Ciphersuite, using a smaller +/// threshold than the original one. +pub fn check_refresh_shares_with_dkg_smaller_threshold< + C: Ciphersuite + PartialEq, + R: RngCore + CryptoRng, +>( + mut rng: R, +) where + C::Group: core::cmp::PartialEq, +{ + //////////////////////////////////////////////////////////////////////////// + // Old Key generation + //////////////////////////////////////////////////////////////////////////// + + let old_max_signers = 5; + let min_signers = 3; + let (old_shares, pub_key_package) = generate_with_dealer( + old_max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + let mut old_key_packages: BTreeMap, KeyPackage> = BTreeMap::new(); + + for (k, v) in old_shares { + let key_package = KeyPackage::try_from(v).unwrap(); + old_key_packages.insert(k, key_package); + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, Round 1 + //////////////////////////////////////////////////////////////////////////// + + let max_signers = 4; + // Use a smaller threshold than the original + let min_signers = 2; + + let remaining_ids = vec![ + Identifier::try_from(4).unwrap(), + Identifier::try_from(2).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(1).unwrap(), + ]; + + // Keep track of each participant's round 1 secret package. + // In practice each participant will keep its copy; no one + // will have all the participant's packages. + let mut round1_secret_packages: BTreeMap< + frost::Identifier, + frost::keys::dkg::round1::SecretPackage, + > = BTreeMap::new(); + + // Keep track of all round 1 packages sent to the given participant. + // This is used to simulate the broadcast; in practice the packages + // will be sent through some communication channel. + let mut received_round1_packages: BTreeMap< + frost::Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + > = BTreeMap::new(); + + // For each participant, perform the first part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_identifier in remaining_ids.clone() { + let (round1_secret_package, round1_package) = + refresh_dkg_part1(participant_identifier, max_signers, min_signers, &mut rng).unwrap(); + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round1_secret_packages.insert(participant_identifier, round1_secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + for receiver_participant_identifier in remaining_ids.clone() { + if receiver_participant_identifier == participant_identifier { + continue; + } + received_round1_packages + .entry(receiver_participant_identifier) + .or_default() + .insert(participant_identifier, round1_package.clone()); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, Round 2 + //////////////////////////////////////////////////////////////////////////// + // Keep track of each participant's round 2 secret package. + // In practice each participant will keep its copy; no one + // will have all the participant's packages. + let mut round2_secret_packages = BTreeMap::new(); + + // Keep track of all round 2 packages sent to the given participant. + // This is used to simulate the broadcast; in practice the packages + // will be sent through some communication channel. + let mut received_round2_packages = BTreeMap::new(); + + // For each participant, perform the second part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_identifier in remaining_ids.clone() { + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + let (round2_secret_package, round2_packages) = + refresh_dkg_part2(round1_secret_package, round1_packages).expect("should work"); + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round2_secret_packages.insert(participant_identifier, round2_secret_package); + + // "Send" the round 2 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + // Note that, in contrast to the previous part, here each other participant + // gets its own specific package. + for (receiver_identifier, round2_package) in round2_packages { + received_round2_packages + .entry(receiver_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round2_package); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Key generation, final computation + //////////////////////////////////////////////////////////////////////////// + + // For each participant, this is where they refresh their shares + // In practice, each participant will perform this on their own environments. + let mut results = Vec::new(); + for participant_identifier in remaining_ids.clone() { + results.push(frost::keys::refresh::refresh_dkg_shares( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages[&participant_identifier], + pub_key_package.clone(), + old_key_packages[&participant_identifier].clone(), + )); + } + + assert!(results + .iter() + .all(|r| matches!(r, Err(Error::InvalidMinSigners)))); +} diff --git a/frost-core/src/tests/repairable.rs b/frost-core/src/tests/repairable.rs index 8c187a859..e4379732e 100644 --- a/frost-core/src/tests/repairable.rs +++ b/frost-core/src/tests/repairable.rs @@ -1,22 +1,24 @@ //! Test for Repairable Threshold Scheme -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; -use debugless_unwrap::DebuglessUnwrap; +use debugless_unwrap::DebuglessUnwrapExt; use rand_core::{CryptoRng, RngCore}; use serde_json::Value; use crate as frost; +use crate::keys::repairable::{Delta, Sigma}; +use crate::keys::KeyPackage; use crate::{ compute_lagrange_coefficient, keys::{ - repairable::{repair_share_step_1, repair_share_step_2, repair_share_step_3}, - PublicKeyPackage, SecretShare, SigningShare, + repairable::{repair_share_part1, repair_share_part2, repair_share_part3}, + PublicKeyPackage, SecretShare, }, - Ciphersuite, Error, Field, Group, Identifier, Scalar, + Ciphersuite, Error, Field, Group, Identifier, }; -/// We want to test that recover share matches the original share +/// We want to test that recovered share matches the original share pub fn check_rts(mut rng: R) { // Compute shares @@ -26,24 +28,30 @@ pub fn check_rts(mut rng: R) { let max_signers = 5; let min_signers = 3; - let (shares, _pubkeys): (BTreeMap, SecretShare>, PublicKeyPackage) = - frost::keys::generate_with_dealer( - max_signers, - min_signers, - frost::keys::IdentifierList::Default, - &mut rng, - ) - .unwrap(); + let (shares, public_key_package): ( + BTreeMap, SecretShare>, + PublicKeyPackage, + ) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + let key_packages = shares + .into_iter() + .map(|(id, share)| (id, share.try_into().unwrap())) + .collect::, KeyPackage>>(); // Try to recover a share // Signer 2 will lose their share // Signer 1, 4 and 5 will help signer 2 to recover their share - let helper_1 = &shares[&Identifier::try_from(1).unwrap()]; - let helper_4 = &shares[&Identifier::try_from(4).unwrap()]; - let helper_5 = &shares[&Identifier::try_from(5).unwrap()]; - let participant = &shares[&Identifier::try_from(2).unwrap()]; + let helper_1 = &key_packages[&Identifier::try_from(1).unwrap()]; + let helper_4 = &key_packages[&Identifier::try_from(4).unwrap()]; + let helper_5 = &key_packages[&Identifier::try_from(5).unwrap()]; + let participant = &key_packages[&Identifier::try_from(2).unwrap()]; let helpers: [Identifier; 3] = [ helper_1.identifier, @@ -54,25 +62,25 @@ pub fn check_rts(mut rng: R) { // Each helper generates random values for each helper let helper_1_deltas = - repair_share_step_1(&helpers, helper_1, &mut rng, participant.identifier).unwrap(); + repair_share_part1(&helpers, helper_1, &mut rng, participant.identifier).unwrap(); let helper_4_deltas = - repair_share_step_1(&helpers, helper_4, &mut rng, participant.identifier).unwrap(); + repair_share_part1(&helpers, helper_4, &mut rng, participant.identifier).unwrap(); let helper_5_deltas = - repair_share_step_1(&helpers, helper_5, &mut rng, participant.identifier).unwrap(); + repair_share_part1(&helpers, helper_5, &mut rng, participant.identifier).unwrap(); // Each helper calculates their sigma from the random values received from the other helpers - let helper_1_sigma: Scalar = repair_share_step_2::(&[ + let helper_1_sigma: Sigma = repair_share_part2::(&[ helper_1_deltas[&helpers[0]], helper_4_deltas[&helpers[0]], helper_5_deltas[&helpers[0]], ]); - let helper_4_sigma: Scalar = repair_share_step_2::(&[ + let helper_4_sigma: Sigma = repair_share_part2::(&[ helper_1_deltas[&helpers[1]], helper_4_deltas[&helpers[1]], helper_5_deltas[&helpers[1]], ]); - let helper_5_sigma: Scalar = repair_share_step_2::(&[ + let helper_5_sigma: Sigma = repair_share_part2::(&[ helper_1_deltas[&helpers[2]], helper_4_deltas[&helpers[2]], helper_5_deltas[&helpers[2]], @@ -80,26 +88,28 @@ pub fn check_rts(mut rng: R) { // The participant wishing to recover their share sums the sigmas sent from all helpers - let participant_recovered_share = repair_share_step_3( + let participant_recovered_share = repair_share_part3( &[helper_1_sigma, helper_4_sigma, helper_5_sigma], participant.identifier, - &participant.commitment, - ); + &public_key_package, + ) + .unwrap(); - // TODO: assert on commitment equality as well once updates have been made to VerifiableSecretSharingCommitment assert!(participant.signing_share() == participant_recovered_share.signing_share()) } fn generate_scalar_from_byte_string( bs: &str, -) -> <<::Group as Group>::Field as Field>::Scalar { +) -> <::Field as Field>::Scalar { let decoded = hex::decode(bs).unwrap(); - let out = <::Field>::deserialize(&decoded.try_into().debugless_unwrap()); + let out = <::Field>::deserialize( + &decoded.as_slice().try_into().debugless_unwrap(), + ); out.unwrap() } -/// Test repair_share_step_1 -pub fn check_repair_share_step_1(mut rng: R) { +/// Test repair_share_part1 +pub fn check_repair_share_part1(mut rng: R) { // Compute shares let max_signers = 5; @@ -112,14 +122,18 @@ pub fn check_repair_share_step_1(mut rng &mut rng, ) .unwrap(); + let key_packages = shares + .into_iter() + .map(|(id, share)| (id, share.try_into().unwrap())) + .collect::, KeyPackage>>(); // Signer 2 will lose their share // Signers (helpers) 1, 4 and 5 will help signer 2 (participant) to recover their share - let helper_1 = &shares[&Identifier::try_from(1).unwrap()]; - let helper_4 = &shares[&Identifier::try_from(4).unwrap()]; - let helper_5 = &shares[&Identifier::try_from(5).unwrap()]; - let participant = &shares[&Identifier::try_from(2).unwrap()]; + let helper_1 = &key_packages[&Identifier::try_from(1).unwrap()]; + let helper_4 = &key_packages[&Identifier::try_from(4).unwrap()]; + let helper_5 = &key_packages[&Identifier::try_from(5).unwrap()]; + let participant = &key_packages[&Identifier::try_from(2).unwrap()]; let helpers: [Identifier; 3] = [ helper_1.identifier, @@ -128,7 +142,7 @@ pub fn check_repair_share_step_1(mut rng ]; // Generate deltas for helper 4 - let deltas = repair_share_step_1(&helpers, helper_4, &mut rng, participant.identifier).unwrap(); + let deltas = repair_share_part1(&helpers, helper_4, &mut rng, participant.identifier).unwrap(); let lagrange_coefficient = compute_lagrange_coefficient( &helpers.iter().cloned().collect(), @@ -139,78 +153,85 @@ pub fn check_repair_share_step_1(mut rng let mut rhs = <::Field>::zero(); for (_k, v) in deltas { - rhs = rhs + v; + rhs = rhs + v.to_scalar(); } - let lhs = lagrange_coefficient * helper_4.signing_share.0; + let lhs = lagrange_coefficient * helper_4.signing_share.to_scalar(); assert!(lhs == rhs) } -/// Test repair_share_step_2 -pub fn check_repair_share_step_2(repair_share_helpers: &Value) { +/// Test repair_share_part2 +pub fn check_repair_share_part2(repair_share_helpers: &Value) { let values = &repair_share_helpers["scalar_generation"]; - let value_1 = - generate_scalar_from_byte_string::(values["random_scalar_1"].as_str().unwrap()); - let value_2 = - generate_scalar_from_byte_string::(values["random_scalar_2"].as_str().unwrap()); - let value_3 = - generate_scalar_from_byte_string::(values["random_scalar_3"].as_str().unwrap()); - - let expected: Scalar = repair_share_step_2::(&[value_1, value_2, value_3]); - - let actual: <<::Group as Group>::Field as Field>::Scalar = - generate_scalar_from_byte_string::(values["random_scalar_sum"].as_str().unwrap()); + let value_1 = Delta::new(generate_scalar_from_byte_string::( + values["random_scalar_1"].as_str().unwrap(), + )); + let value_2 = Delta::new(generate_scalar_from_byte_string::( + values["random_scalar_2"].as_str().unwrap(), + )); + let value_3 = Delta::new(generate_scalar_from_byte_string::( + values["random_scalar_3"].as_str().unwrap(), + )); + let expected = repair_share_part2::(&[value_1, value_2, value_3]); + + let actual = Sigma::new(generate_scalar_from_byte_string::( + values["random_scalar_sum"].as_str().unwrap(), + )); assert!(actual == expected); } -/// Test repair_share -pub fn check_repair_share_step_3( +/// Test repair_share_part3 +pub fn check_repair_share_part3( mut rng: R, repair_share_helpers: &Value, ) { - // Generate shares + // We need a dummy public key package to call the function let max_signers = 5; let min_signers = 3; - let (shares, _pubkeys): (BTreeMap, SecretShare>, PublicKeyPackage) = - frost::keys::generate_with_dealer( - max_signers, - min_signers, - frost::keys::IdentifierList::Default, - &mut rng, - ) - .unwrap(); + let (_shares, public_key_package): ( + BTreeMap, SecretShare>, + PublicKeyPackage, + ) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); let sigmas: &Value = &repair_share_helpers["sigma_generation"]; - let sigma_1 = generate_scalar_from_byte_string::(sigmas["sigma_1"].as_str().unwrap()); - let sigma_2 = generate_scalar_from_byte_string::(sigmas["sigma_2"].as_str().unwrap()); - let sigma_3 = generate_scalar_from_byte_string::(sigmas["sigma_3"].as_str().unwrap()); - let sigma_4 = generate_scalar_from_byte_string::(sigmas["sigma_4"].as_str().unwrap()); - - let commitment = (shares[&Identifier::try_from(1).unwrap()].commitment).clone(); - - let expected = repair_share_step_3::( + let sigma_1 = Sigma::new(generate_scalar_from_byte_string::( + sigmas["sigma_1"].as_str().unwrap(), + )); + let sigma_2 = Sigma::new(generate_scalar_from_byte_string::( + sigmas["sigma_2"].as_str().unwrap(), + )); + let sigma_3 = Sigma::new(generate_scalar_from_byte_string::( + sigmas["sigma_3"].as_str().unwrap(), + )); + let sigma_4 = Sigma::new(generate_scalar_from_byte_string::( + sigmas["sigma_4"].as_str().unwrap(), + )); + + let actual = repair_share_part3::( &[sigma_1, sigma_2, sigma_3, sigma_4], Identifier::try_from(2).unwrap(), - &commitment, - ); + &public_key_package, + ) + .unwrap(); - let actual_sigma: <<::Group as Group>::Field as Field>::Scalar = + let expected: <::Field as Field>::Scalar = generate_scalar_from_byte_string::(sigmas["sigma_sum"].as_str().unwrap()); - let actual: SecretShare = SecretShare::new( - Identifier::try_from(2).unwrap(), - SigningShare(actual_sigma), - commitment, - ); - assert!(actual.signing_share == expected.signing_share); + assert!(expected == actual.signing_share().to_scalar()); } -/// Test repair share step 1 fails with invalid numbers of signers. -pub fn check_repair_share_step_1_fails_with_invalid_min_signers< +/// Test repair share part 1 fails with invalid numbers of signers. +pub fn check_repair_share_part1_fails_with_invalid_min_signers< C: Ciphersuite, R: RngCore + CryptoRng, >( @@ -227,16 +248,20 @@ pub fn check_repair_share_step_1_fails_with_invalid_min_signers< &mut rng, ) .unwrap(); + let key_packages = shares + .into_iter() + .map(|(id, share)| (id, share.try_into().unwrap())) + .collect::, KeyPackage>>(); let helper = Identifier::try_from(3).unwrap(); - let out = repair_share_step_1( + let out = repair_share_part1( &[helper], - &shares[&helper], + &key_packages[&helper], &mut rng, Identifier::try_from(2).unwrap(), ); assert!(out.is_err()); - assert!(out == Err(Error::InvalidMinSigners)) + assert!(out == Err(Error::IncorrectNumberOfIdentifiers)) } diff --git a/frost-core/src/tests/vectors.rs b/frost-core/src/tests/vectors.rs index d38f7af3c..1ca8a97bb 100644 --- a/frost-core/src/tests/vectors.rs +++ b/frost-core/src/tests/vectors.rs @@ -1,7 +1,7 @@ //! Helper function for testing with test vectors. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; -use debugless_unwrap::DebuglessUnwrap; +use debugless_unwrap::DebuglessUnwrapExt; use hex::{self, FromHex}; use serde_json::Value; @@ -34,8 +34,7 @@ pub fn parse_test_vectors(json_vectors: &Value) -> TestVectors(json_vectors: &Value) -> TestVectors::Field>::deserialize(&vec.try_into().debugless_unwrap()).unwrap() + <::Field>::deserialize(&vec.as_slice().try_into().debugless_unwrap()) + .unwrap() }) .collect(); @@ -136,12 +136,9 @@ pub fn parse_test_vectors(json_vectors: &Value) -> TestVectors::Field as Field>::Serialization::try_from( - hex::decode(signer["sig_share"].as_str().unwrap()).unwrap(), - ) - .debugless_unwrap(); + let sig_share = hex::decode(signer["sig_share"].as_str().unwrap()).unwrap(); - let signature_share = SignatureShare::::deserialize(sig_share).unwrap(); + let signature_share = SignatureShare::::deserialize(&sig_share).unwrap(); signature_shares.insert(i.try_into().unwrap(), signature_share); } @@ -263,13 +260,14 @@ pub fn check_sign_with_test_vectors(json_vectors: &Value) { for (identifier, input) in signing_package .binding_factor_preimages(&verifying_key, &[]) + .unwrap() .iter() { assert_eq!(*input, binding_factor_inputs[identifier]); } let binding_factor_list: frost::BindingFactorList = - compute_binding_factor_list(&signing_package, &verifying_key, &[]); + compute_binding_factor_list(&signing_package, &verifying_key, &[]).unwrap(); for (identifier, binding_factor) in binding_factor_list.0.iter() { assert_eq!(*binding_factor, binding_factors[identifier]); @@ -295,7 +293,11 @@ pub fn check_sign_with_test_vectors(json_vectors: &Value) { .map(|(i, key_package)| (i, *key_package.verifying_share())) .collect(); - let pubkey_package = frost::keys::PublicKeyPackage::new(verifying_shares, verifying_key); + let pubkey_package = frost::keys::PublicKeyPackage::new( + verifying_shares, + verifying_key, + Some(min_signers as u16), + ); //////////////////////////////////////////////////////////////////////////// // Aggregation: collects the signing shares from all participants, @@ -311,7 +313,10 @@ pub fn check_sign_with_test_vectors(json_vectors: &Value) { // Check that the generated signature matches the test vector signature let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.serialize().as_ref(), signature_bytes); + assert_eq!( + group_signature.serialize().unwrap().as_ref(), + signature_bytes + ); // Aggregate the FROST signature from our signature shares let group_signature_result = @@ -322,5 +327,8 @@ pub fn check_sign_with_test_vectors(json_vectors: &Value) { // Check that the generated signature matches the test vector signature let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.serialize().as_ref(), signature_bytes); + assert_eq!( + group_signature.serialize().unwrap().as_ref(), + signature_bytes + ); } diff --git a/frost-core/src/tests/vectors_dkg.rs b/frost-core/src/tests/vectors_dkg.rs index 1b06a21a1..6f1ce5353 100644 --- a/frost-core/src/tests/vectors_dkg.rs +++ b/frost-core/src/tests/vectors_dkg.rs @@ -1,7 +1,10 @@ //! Helper function for testing with test vectors. -use std::collections::BTreeMap; - -use debugless_unwrap::DebuglessUnwrap; +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, + vec::Vec, +}; +use debugless_unwrap::DebuglessUnwrapExt; use hex::{self}; use serde_json::Value; @@ -32,100 +35,118 @@ fn json_to_scalar( vector: &Value, ) -> <::Field as Field>::Serialization { (hex::decode(vector.as_str().unwrap()).unwrap()) + .as_slice() .try_into() .debugless_unwrap() } fn json_to_element(vector: &Value) -> ::Serialization { (hex::decode(vector.as_str().unwrap()).unwrap()) + .as_slice() .try_into() .debugless_unwrap() } /// Parse test vectors for a given ciphersuite. #[allow(clippy::type_complexity)] -pub fn parse_test_vectors_dkg(json_vectors: &Value) -> DKGTestVectors { +pub fn parse_test_vectors_dkg(json_vectors: &Value) -> Vec> { + let mut vectors: Vec> = Vec::new(); let inputs = &json_vectors["inputs"]; - let participant = &inputs["1"]; - - let participant_1_id: Identifier = (participant["identifier"].as_u64().unwrap() as u16) - .try_into() - .unwrap(); - let participant_2_id: Identifier = (inputs["2"]["identifier"].as_u64().unwrap() as u16) - .try_into() - .unwrap(); - let participant_3_id: Identifier = (inputs["3"]["identifier"].as_u64().unwrap() as u16) - .try_into() - .unwrap(); - - let mut round1_packages = BTreeMap::new(); - round1_packages.insert(participant_2_id, build_round_1_package(json_vectors, 2)); - round1_packages.insert(participant_3_id, build_round_1_package(json_vectors, 3)); + let max_participants = json_vectors["config"]["MAX_PARTICIPANTS"].as_u64().unwrap() as u16; + let min_signers = json_vectors["config"]["MIN_PARTICIPANTS"].as_u64().unwrap() as u16; - let mut round2_packages = BTreeMap::new(); - round2_packages.insert(participant_2_id, build_round_2_package(json_vectors, 2)); - round2_packages.insert(participant_3_id, build_round_2_package(json_vectors, 3)); - - let secret = SigningKey::deserialize(json_to_scalar::(&participant["signing_key"])).unwrap(); + for i in 1..=max_participants { + let participant_id_str = &i.to_string(); + let participant_data = &inputs[participant_id_str]; + let participant_id: Identifier = (participant_data["identifier"].as_u64().unwrap() + as u16) + .try_into() + .unwrap(); - let coefficient = <::Field as Field>::deserialize(&json_to_scalar::( - &participant["coefficient"], - )) - .unwrap(); + let mut round1_packages = BTreeMap::new(); + let mut round2_packages = BTreeMap::new(); + for (other_participant_id_str, other_participant_data) in inputs.as_object().unwrap() { + if participant_id_str == other_participant_id_str { + continue; + } + match other_participant_id_str.parse::() { + Ok(id) => id, + Err(_) => continue, + }; + let other_participant_id: Identifier = + (other_participant_data["identifier"].as_u64().unwrap() as u16) + .try_into() + .unwrap(); + round1_packages.insert( + other_participant_id, + build_round_1_package(other_participant_data), + ); + round2_packages.insert( + other_participant_id, + build_round_2_package(participant_data, other_participant_id_str), + ); + } + + let secret = + SigningKey::deserialize(json_to_scalar::(&participant_data["signing_key"]).as_ref()) + .unwrap(); + + let coefficient = <::Field as Field>::deserialize(&json_to_scalar::( + &participant_data["coefficient"], + )) + .unwrap(); - let public_key_package = build_public_key_package(json_vectors); + let public_key_package = build_public_key_package(json_vectors); - let verifying_share = - VerifyingShare::deserialize(json_to_element::(&participant["verifying_share"])).unwrap(); + let verifying_share = VerifyingShare::deserialize( + json_to_element::(&participant_data["verifying_share"]).as_ref(), + ) + .unwrap(); - let verifying_key = - VerifyingKey::deserialize(json_to_element::(&inputs["verifying_key"])).unwrap(); + let verifying_key = + VerifyingKey::deserialize(json_to_element::(&inputs["verifying_key"]).as_ref()) + .unwrap(); - let signing_share = - SigningShare::deserialize(json_to_scalar::(&participant["signing_share"])).unwrap(); + let signing_share = SigningShare::deserialize( + json_to_scalar::(&participant_data["signing_share"]).as_ref(), + ) + .unwrap(); - let key_package = KeyPackage { - header: Header::default(), - identifier: participant_1_id, - signing_share, - verifying_share, - verifying_key, - min_signers: 2, - }; - - DKGTestVectors { - secret, - coefficient, - round1_packages, - round2_packages, - public_key_package, - key_package, - participant_id: participant_1_id, + let key_package = KeyPackage { + header: Header::default(), + identifier: participant_id, + signing_share, + verifying_share, + verifying_key, + min_signers, + }; + vectors.push(DKGTestVectors { + secret, + coefficient, + round1_packages, + round2_packages, + public_key_package, + key_package, + participant_id, + }) } + vectors } -fn build_round_1_package( - json_vectors: &Value, - participant_num: usize, -) -> Round1Package { - let inputs = &json_vectors["inputs"]; - let participant = &inputs[participant_num.to_string()]; - let vss_commitment = participant["vss_commitments"] +fn build_round_1_package(json_vectors: &Value) -> Round1Package { + let vss_commitment = json_vectors["vss_commitments"] .as_array() .unwrap() .iter() - .map(|v| json_to_element::(v)) - .collect(); + .map(|v| json_to_element::(v).as_ref().to_vec()) + .collect::>>(); let commitment = VerifiableSecretSharingCommitment::deserialize(vss_commitment).unwrap(); let proof_of_knowledge = Signature::deserialize( - C::SignatureSerialization::try_from( - hex::decode(participant["proof_of_knowledge"].as_str().unwrap()).unwrap(), - ) - .debugless_unwrap(), + &hex::decode(json_vectors["proof_of_knowledge"].as_str().unwrap()).unwrap(), ) - .unwrap(); + .debugless_unwrap(); Round1Package { header: Header::default(), @@ -136,13 +157,11 @@ fn build_round_1_package( fn build_round_2_package( json_vectors: &Value, - sender_num: usize, + sender_num: &String, ) -> Round2Package { - let inputs = &json_vectors["inputs"]; - - let signing_share = SigningShare::deserialize(json_to_scalar::( - &inputs["1"]["signing_shares"][sender_num.to_string()], - )) + let signing_share = SigningShare::deserialize( + json_to_scalar::(&json_vectors["signing_shares"][sender_num]).as_ref(), + ) .unwrap(); Round2Package { @@ -157,66 +176,70 @@ fn build_public_key_package(json_vectors: &Value) -> PublicKeyPa let mut verifying_shares = BTreeMap::new(); let max_participants = json_vectors["config"]["MAX_PARTICIPANTS"].as_u64().unwrap() as u8; + let min_participants = json_vectors["config"]["MIN_PARTICIPANTS"].as_u64().unwrap() as u8; for i in 1..=max_participants { let participant_id: Identifier = (inputs[i.to_string()]["identifier"].as_u64().unwrap() as u16) .try_into() .unwrap(); - let verifying_share = VerifyingShare::deserialize(json_to_element::( - &inputs[i.to_string()]["verifying_share"], - )) + let verifying_share = VerifyingShare::deserialize( + json_to_element::(&inputs[i.to_string()]["verifying_share"]).as_ref(), + ) .unwrap(); verifying_shares.insert(participant_id, verifying_share); } let verifying_key = - VerifyingKey::deserialize(json_to_element::(&inputs["verifying_key"])).unwrap(); + VerifyingKey::deserialize(json_to_element::(&inputs["verifying_key"]).as_ref()).unwrap(); PublicKeyPackage { header: Header::default(), verifying_shares, verifying_key, + min_signers: Some(min_participants as u16), } } /// Test DKG with the given test vectors for a ciphersuite pub fn check_dkg_keygen(json_vectors: &Value) { - let DKGTestVectors { - secret, - coefficient, - round1_packages, - round2_packages, - public_key_package, - key_package, - participant_id, - } = parse_test_vectors_dkg(json_vectors); - - let min_signers = 2; - let max_signers = 3; - - let (coefficients, commitment) = generate_secret_polynomial( - &secret as &SigningKey, - max_signers, - min_signers, - vec![coefficient], - ) - .unwrap(); + for dkg_vectors in parse_test_vectors_dkg(json_vectors) { + let DKGTestVectors { + secret, + coefficient, + round1_packages, + round2_packages, + public_key_package, + key_package, + participant_id, + } = dkg_vectors; + + let min_signers = 2; + let max_signers = 3; + + let (coefficients, commitment) = generate_secret_polynomial( + &secret as &SigningKey, + max_signers, + min_signers, + vec![coefficient], + ) + .unwrap(); - let round1_secret_package = SecretPackage { - identifier: participant_id, - coefficients, - commitment: commitment.clone(), - min_signers, - max_signers, - }; + let round1_secret_package = SecretPackage::new( + participant_id, + coefficients, + commitment.clone(), + min_signers, + max_signers, + ); - let (round2_secret_package, _round2_packages_1) = - part2(round1_secret_package, &round1_packages).unwrap(); + let (round2_secret_package, _round2_packages_1) = + part2(round1_secret_package, &round1_packages).unwrap(); - let (expected_key_package, expected_public_key_package) = - part3(&round2_secret_package, &round1_packages, &round2_packages).unwrap(); + let (expected_key_package, expected_public_key_package) = + part3(&round2_secret_package, &round1_packages, &round2_packages).unwrap(); - assert_eq!(public_key_package, expected_public_key_package); - assert_eq!(key_package, expected_key_package); + assert_eq!(public_key_package, expected_public_key_package); + assert_eq!(key_package, expected_key_package); + } } diff --git a/frost-core/src/tests/vss_commitment.rs b/frost-core/src/tests/vss_commitment.rs index 562affc03..62a27556f 100644 --- a/frost-core/src/tests/vss_commitment.rs +++ b/frost-core/src/tests/vss_commitment.rs @@ -1,13 +1,12 @@ //! VerifiableSecretSharingCommitment functions -use std::convert::TryFrom; - use crate::{ keys::{CoefficientCommitment, VerifiableSecretSharingCommitment}, tests::helpers::generate_element, - Group, + Error, Group, }; -use debugless_unwrap::DebuglessUnwrap; +use alloc::vec::Vec; +use debugless_unwrap::DebuglessUnwrapExt; use rand_core::{CryptoRng, RngCore}; use serde_json::Value; @@ -24,26 +23,62 @@ pub fn check_serialize_vss_commitment(mu let input_3 = generate_element::(&mut rng); let coeff_comms = vec![ - CoefficientCommitment::(input_1), - CoefficientCommitment(input_2), - CoefficientCommitment(input_3), + CoefficientCommitment::::new(input_1), + CoefficientCommitment::new(input_2), + CoefficientCommitment::new(input_3), ]; // --- - let expected = vec![ - ::serialize(&input_1), - ::serialize(&input_2), - ::serialize(&input_3), + let expected = [ + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), ]; - let vss_commitment = VerifiableSecretSharingCommitment(coeff_comms).serialize(); + let vss_commitment = VerifiableSecretSharingCommitment(coeff_comms) + .serialize() + .unwrap(); assert!(expected.len() == vss_commitment.len()); assert!(expected .iter() .zip(vss_commitment.iter()) - .all(|(e, c)| e.as_ref() == c.as_ref())); + .all(|(e, c)| e.as_ref() == c)); +} + +/// Test serialize_whole VerifiableSecretSharingCommitment +pub fn check_serialize_whole_vss_commitment(mut rng: R) { + // Generate test CoefficientCommitments + + // --- + let input_1 = generate_element::(&mut rng); + let input_2 = generate_element::(&mut rng); + let input_3 = generate_element::(&mut rng); + + let coeff_comms = vec![ + CoefficientCommitment::::new(input_1), + CoefficientCommitment::new(input_2), + CoefficientCommitment::new(input_3), + ]; + + // --- + + let expected = [ + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), + ] + .into_iter() + .map(|element| element.as_ref().to_vec()) + .collect::>() + .concat(); + + let vss_commitment = VerifiableSecretSharingCommitment(coeff_comms) + .serialize_whole() + .unwrap(); + + assert!(expected == vss_commitment); } /// Test deserialize VerifiableSecretSharingCommitment @@ -56,18 +91,18 @@ pub fn check_deserialize_vss_commitment( let input_3 = generate_element::(&mut rng); let coeff_comms = vec![ - CoefficientCommitment::(input_1), - CoefficientCommitment(input_2), - CoefficientCommitment(input_3), + CoefficientCommitment::::new(input_1), + CoefficientCommitment::new(input_2), + CoefficientCommitment::new(input_3), ]; // --- let expected = VerifiableSecretSharingCommitment(coeff_comms); let data = vec![ - ::serialize(&input_1), - ::serialize(&input_2), - ::serialize(&input_3), + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), ]; let vss_value = VerifiableSecretSharingCommitment::deserialize(data); @@ -76,6 +111,40 @@ pub fn check_deserialize_vss_commitment( assert!(expected == vss_value.unwrap()); } +/// Test deserialize_whole VerifiableSecretSharingCommitment +pub fn check_deserialize_whole_vss_commitment(mut rng: R) { + // Generate test CoefficientCommitments + + // --- + let input_1 = generate_element::(&mut rng); + let input_2 = generate_element::(&mut rng); + let input_3 = generate_element::(&mut rng); + + let coeff_comms = vec![ + CoefficientCommitment::::new(input_1), + CoefficientCommitment::new(input_2), + CoefficientCommitment::new(input_3), + ]; + // --- + + let expected = VerifiableSecretSharingCommitment(coeff_comms); + + let data = vec![ + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), + ] + .into_iter() + .map(|element| element.as_ref().to_vec()) + .collect::>() + .concat(); + + let vss_value = VerifiableSecretSharingCommitment::deserialize_whole(&data); + + assert!(vss_value.is_ok()); + assert!(expected == vss_value.unwrap()); +} + /// Test deserialize VerifiableSecretSharingCommitment error pub fn check_deserialize_vss_commitment_error( mut rng: R, @@ -92,15 +161,15 @@ pub fn check_deserialize_vss_commitment_error::Serialization = ::Serialization::try_from( - hex::decode(values["invalid_element"].as_str().unwrap()).unwrap(), + &hex::decode(values["invalid_element"].as_str().unwrap()).unwrap(), ) .debugless_unwrap(); // --- let data = vec![ - ::serialize(&input_1), - ::serialize(&input_2), - ::serialize(&input_3), + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), serialized, ]; @@ -109,6 +178,60 @@ pub fn check_deserialize_vss_commitment_error( + mut rng: R, + commitment_helpers: &Value, +) { + // Generate test CoefficientCommitments + + // --- + let values = &commitment_helpers["elements"]; + + let input_1 = generate_element::(&mut rng); + let input_2 = generate_element::(&mut rng); + let input_3 = generate_element::(&mut rng); + + let serialized: ::Serialization = + ::Serialization::try_from( + &hex::decode(values["invalid_element"].as_str().unwrap()).unwrap(), + ) + .debugless_unwrap(); + // --- + + let data = vec![ + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), + serialized, + ] + .into_iter() + .map(|element| element.as_ref().to_vec()) + .collect::>() + .concat(); + + let vss_value = VerifiableSecretSharingCommitment::::deserialize_whole(&data); + + assert!(vss_value.is_err()); + + // Generate test CoefficientCommitments with invalid length + + let mut data = vec![ + ::serialize(&input_1).unwrap(), + ::serialize(&input_2).unwrap(), + ::serialize(&input_3).unwrap(), + ] + .into_iter() + .map(|element| element.as_ref().to_vec()) + .collect::>() + .concat(); + data.append(&mut vec![0x00]); + + let vss_value = VerifiableSecretSharingCommitment::::deserialize_whole(&data); + + assert_eq!(vss_value, Err(Error::InvalidCoefficient)); +} + /// Test computing the public key package from a list of commitments. pub fn check_compute_public_key_package(mut rng: R) { let max_signers = 3; diff --git a/frost-core/src/traits.rs b/frost-core/src/traits.rs index 817caf60d..9ae94319f 100644 --- a/frost-core/src/traits.rs +++ b/frost-core/src/traits.rs @@ -1,13 +1,22 @@ //! Traits used to abstract Ciphersuites. -use std::{ +use core::{ fmt::Debug, ops::{Add, Mul, Sub}, }; +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; use rand_core::{CryptoRng, RngCore}; -use crate::{Error, FieldError, GroupError, Signature, VerifyingKey}; +use crate::{ + challenge, + keys::{KeyPackage, PublicKeyPackage, SecretShare, VerifyingShare}, + random_nonzero, + round1::{self, SigningNonces}, + round2::{self, SignatureShare}, + BindingFactor, BindingFactorList, Challenge, Error, FieldError, GroupCommitment, GroupError, + Identifier, Signature, SigningKey, SigningPackage, VerifyingKey, +}; /// A prime order finite field GF(q) over which all scalar values for our prime order group can be /// multiplied are defined. @@ -16,7 +25,7 @@ use crate::{Error, FieldError, GroupError, Signature, VerifyingKey}; /// pass-through, implemented for a type just for the ciphersuite, and calls through to another /// implementation underneath, so that this trait does not have to be implemented for types you /// don't own. -pub trait Field: Copy + Clone { +pub trait Field: Copy { /// An element of the scalar field GF(p). /// The Eq/PartialEq implementation MUST be constant-time. type Scalar: Add @@ -25,10 +34,12 @@ pub trait Field: Copy + Clone { + Eq + Mul + PartialEq - + Sub; + + Sub + + Send + + Sync; /// A unique byte array buf of fixed length N. - type Serialization: AsRef<[u8]> + Debug + TryFrom>; + type Serialization: Clone + AsRef<[u8]> + AsMut<[u8]> + for<'a> TryFrom<&'a [u8]> + Debug; /// Returns the zero element of the field, the additive identity. fn zero() -> Self::Scalar; @@ -42,13 +53,13 @@ pub trait Field: Copy + Clone { /// Generate a random scalar from the entire space [0, l-1] /// - /// + /// fn random(rng: &mut R) -> Self::Scalar; /// A member function of a [`Field`] that maps a [`Scalar`] to a unique byte array buf of /// fixed length Ne. /// - /// + /// fn serialize(scalar: &Self::Scalar) -> Self::Serialization; /// A member function of a [`Field`] that maps a [`Scalar`] to a unique byte array buf of @@ -62,7 +73,7 @@ pub trait Field: Copy + Clone { /// Fails if the input is not a valid byte representation of an [`Scalar`] of the /// [`Field`]. This function can raise an [`Error`] if deserialization fails. /// - /// + /// fn deserialize(buf: &Self::Serialization) -> Result; } @@ -76,7 +87,7 @@ pub type Scalar = <<::Group as Group>::Field as Field>::Sca /// pass-through, implemented for a type just for the ciphersuite, and calls through to another /// implementation underneath, so that this trait does not have to be implemented for types you /// don't own. -pub trait Group: Copy + Clone + PartialEq { +pub trait Group: Copy + PartialEq { /// A prime order finite field GF(q) over which all scalar values for our prime order group can /// be multiplied are defined. type Field: Field; @@ -88,12 +99,14 @@ pub trait Group: Copy + Clone + PartialEq { + Eq + Mul<::Scalar, Output = Self::Element> + PartialEq - + Sub; + + Sub + + Send + + Sync; /// A unique byte array buf of fixed length N. /// /// Little-endian! - type Serialization: AsRef<[u8]> + Debug + TryFrom>; + type Serialization: Clone + AsRef<[u8]> + AsMut<[u8]> + for<'a> TryFrom<&'a [u8]> + Debug; /// The order of the the quotient group when the prime order subgroup divides the order of the /// full curve group. @@ -103,21 +116,22 @@ pub trait Group: Copy + Clone + PartialEq { /// Additive [identity] of the prime order group. /// - /// [identity]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-3.1-3.2 + /// [identity]: https://datatracker.ietf.org/doc/html/rfc9591#section-3.1-4.4 fn identity() -> Self::Element; /// The fixed generator element of the prime order group. /// /// The 'base' of ['ScalarBaseMult()'] from the spec. /// - /// [`ScalarBaseMult()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-3.1-3.5 + /// [`ScalarBaseMult()`]: https://datatracker.ietf.org/doc/html/rfc9591#section-3.1-4.10 fn generator() -> Self::Element; - /// A member function of a group _G_ that maps an [`Element`] to a unique byte array buf of - /// fixed length Ne. + /// A member function of a group _G_ that maps an [`Element`] to a unique + /// byte array buf of fixed length Ne. This function raises an error if the + /// element is the identity element of the group. /// - /// - fn serialize(element: &Self::Element) -> Self::Serialization; + /// + fn serialize(element: &Self::Element) -> Result; /// A member function of a [`Group`] that attempts to map a byte array `buf` to an [`Element`]. /// @@ -125,7 +139,7 @@ pub trait Group: Copy + Clone + PartialEq { /// [`Group`]. This function can raise an [`Error`] if deserialization fails or if the /// resulting [`Element`] is the identity element of the group /// - /// + /// fn deserialize(buf: &Self::Serialization) -> Result; } @@ -135,8 +149,9 @@ pub type Element = <::Group as Group>::Element; /// A [FROST ciphersuite] specifies the underlying prime-order group details and cryptographic hash /// function. /// -/// [FROST ciphersuite]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-ciphersuites -pub trait Ciphersuite: Copy + Clone + PartialEq + Debug { +/// [FROST ciphersuite]: https://datatracker.ietf.org/doc/html/rfc9591#name-ciphersuites +// See https://github.com/ZcashFoundation/frost/issues/693 for reasoning about the 'static bound. +pub trait Ciphersuite: Copy + PartialEq + Debug + 'static + Send + Sync { /// The ciphersuite ID string. It should be equal to the contextString in /// the spec. For new ciphersuites, this should be a string that identifies /// the ciphersuite; it's recommended to use a similar format to the @@ -151,34 +166,38 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug { /// A unique byte array of fixed length that is the `Group::ElementSerialization` + /// `Group::ScalarSerialization` - type SignatureSerialization: AsRef<[u8]> + TryFrom>; + type SignatureSerialization: Clone + + AsRef<[u8]> + + AsMut<[u8]> + + for<'a> TryFrom<&'a [u8]> + + Debug; /// [H1] for a FROST ciphersuite. /// /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. /// - /// [H1]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-cryptographic-hash-function + /// [H1]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function fn H1(m: &[u8]) -> <::Field as Field>::Scalar; /// [H2] for a FROST ciphersuite. /// /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. /// - /// [H2]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-cryptographic-hash-function + /// [H2]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function fn H2(m: &[u8]) -> <::Field as Field>::Scalar; /// [H3] for a FROST ciphersuite. /// /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. /// - /// [H3]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-cryptographic-hash-function + /// [H3]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function fn H3(m: &[u8]) -> <::Field as Field>::Scalar; /// [H4] for a FROST ciphersuite. /// /// Usually an an alias for the ciphersuite hash function _H_ with domain separation applied. /// - /// [H4]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-cryptographic-hash-function + /// [H4]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function fn H4(m: &[u8]) -> Self::HashOutput; /// [H5] for a FROST ciphersuite. @@ -210,22 +229,243 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug { None } - /// Verify a signature for this ciphersuite. The default implementation uses the "cofactored" - /// equation (it multiplies by the cofactor returned by [`Group::cofactor()`]). + // The following are optional methods that allow customizing steps of the + // protocol if required. + + /// Optional. Do regular (non-FROST) signing with a [`SigningKey`]. Called + /// by [`SigningKey::sign()`]. This is not used by FROST. Can be overridden + /// if required which is useful if FROST signing has been changed by the + /// other Ciphersuite trait methods and regular signing should be changed + /// accordingly to match. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + signing_key.default_sign(rng, message) + } + + /// Optional. Verify a signature for this ciphersuite. Called by + /// [`VerifyingKey::verify()`]. The default implementation uses the + /// "cofactored" equation (it multiplies by the cofactor returned by + /// [`Group::cofactor()`]). /// /// # Cryptographic Safety /// - /// You may override this to provide a tailored implementation, but if the ciphersuite defines it, - /// it must also multiply by the cofactor to comply with the RFC. Note that batch verification - /// (see [`crate::batch::Verifier`]) also uses the default implementation regardless whether a - /// tailored implementation was provided. + /// You may override this to provide a tailored implementation, but if the + /// ciphersuite defines it, it must also multiply by the cofactor to comply + /// with the RFC. Note that batch verification (see + /// [`crate::batch::Verifier`]) also uses the default implementation + /// regardless whether a tailored implementation was provided. fn verify_signature( - msg: &[u8], + message: &[u8], signature: &Signature, public_key: &VerifyingKey, ) -> Result<(), Error> { - let c = crate::challenge::(&signature.R, public_key, msg); + let (message, signature, public_key) = ::pre_verify(message, signature, public_key)?; + + let c = ::challenge(&signature.R, &public_key, &message)?; + + public_key.verify_prehashed(c, &signature) + } + + /// Optional. Pre-process [`round2::sign()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Borrowed(key_package), + )) + } + + /// Optional. Pre-process [`crate::aggregate()`] and + /// [`crate::verify_signature_share()`] inputs. In the latter case, "dummy" + /// container BTreeMap and PublicKeyPackage are passed with the relevant + /// values (PublicKeyPackage.min_signers will be None). The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, round2::SignatureShare>, + public_key_package: &'a PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap, round2::SignatureShare>>, + Cow<'a, PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Borrowed(public_key_package), + )) + } + + /// Optional. Pre-process [`VerifyingKey::verify()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_verify<'a>( + msg: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result< + ( + Cow<'a, [u8]>, + Cow<'a, Signature>, + Cow<'a, VerifyingKey>, + ), + Error, + > { + Ok(( + Cow::Borrowed(msg), + Cow::Borrowed(signature), + Cow::Borrowed(public_key), + )) + } + + /// Optional. Pre-process [`crate::compute_group_commitment()`] inputs in + /// [`round2::sign()`]. + #[allow(clippy::type_complexity)] + fn pre_commitment_sign<'a>( + signing_package: &'a SigningPackage, + signing_nonces: &'a SigningNonces, + _binding_factor_list: &'a BindingFactorList, + ) -> Result<(Cow<'a, SigningPackage>, Cow<'a, SigningNonces>), Error> { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signing_nonces), + )) + } + + /// Optional. Pre-process [`crate::compute_group_commitment()`] inputs in + /// [`crate::aggregate()`]. + fn pre_commitment_aggregate<'a>( + signing_package: &'a SigningPackage, + _binding_factor_list: &'a BindingFactorList, + ) -> Result>, Error> { + Ok(Cow::Borrowed(signing_package)) + } + + /// Optional. Generate a nonce and a commitment to it. Used by + /// [`SigningKey`] for regular (non-FROST) signing and internally by the DKG + /// to generate proof-of-knowledge signatures. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + (k, R) + } + + /// Optional. Generates the challenge as is required for Schnorr signatures. + /// Called by [`round2::sign()`] and [`crate::aggregate()`]. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + challenge(R, verifying_key, message) + } + + /// Optional. Compute the signature share for a particular signer on a given + /// challenge. Called by [`round2::sign()`]. + fn compute_signature_share( + _group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + round2::compute_signature_share( + signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Optional. Verify a signing share. Called by [`crate::aggregate()`] if + /// cheater detection is enabled. + fn verify_share( + _group_commitment: &GroupCommitment, + signature_share: &SignatureShare, + identifier: Identifier, + group_commitment_share: &round1::GroupCommitmentShare, + verifying_share: &VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + signature_share.verify( + identifier, + group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Optional. Converts a signature to its + /// [`Ciphersuite::SignatureSerialization`] in bytes. + /// + /// The default implementation serializes a signature by serializing its `R` + /// point and `z` component independently, and then concatenating them. + fn serialize_signature(signature: &Signature) -> Result, Error> { + signature.default_serialize() + } + + /// Optional. Converts bytes as [`Ciphersuite::SignatureSerialization`] into + /// a `Signature`. + /// + /// The default implementation assumes the serialization is a serialized `R` + /// point followed by a serialized `z` component with no padding or extra + /// fields. + fn deserialize_signature(bytes: &[u8]) -> Result, Error> { + Signature::::default_deserialize(bytes) + } + + /// Post-process the output of the DKG for a given participant. + fn post_dkg( + key_package: KeyPackage, + public_key_package: PublicKeyPackage, + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + Ok((key_package, public_key_package)) + } - public_key.verify_prehashed(c, signature) + /// Post-process the output of the key generation for a participant. + #[allow(clippy::type_complexity)] + fn post_generate( + secret_shares: BTreeMap, SecretShare>, + public_key_package: PublicKeyPackage, + ) -> Result< + ( + BTreeMap, SecretShare>, + PublicKeyPackage, + ), + Error, + > { + Ok((secret_shares, public_key_package)) } } diff --git a/frost-core/src/verifying_key.rs b/frost-core/src/verifying_key.rs index 519020b58..24f054019 100644 --- a/frost-core/src/verifying_key.rs +++ b/frost-core/src/verifying_key.rs @@ -1,24 +1,22 @@ -use std::fmt::{self, Debug}; +use core::fmt::{self, Debug}; + +use alloc::{string::ToString, vec::Vec}; #[cfg(any(test, feature = "test-impl"))] use hex::FromHex; -use crate::{Challenge, Ciphersuite, Element, Error, Group, Signature}; - -#[cfg(feature = "serde")] -use crate::serialization::ElementSerialization; +use crate::{serialization::SerializableElement, Challenge, Ciphersuite, Error, Group, Signature}; /// A valid verifying key for Schnorr signatures over a FROST [`Ciphersuite::Group`]. #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ElementSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ElementSerialization"))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct VerifyingKey where C: Ciphersuite, { - pub(crate) element: Element, + pub(crate) element: SerializableElement, } impl VerifyingKey @@ -29,31 +27,32 @@ where #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn new(element: ::Element) -> Self { - Self { element } + Self { + element: SerializableElement(element), + } } /// Return the underlying element. - #[cfg(feature = "internals")] - pub fn to_element(self) -> ::Element { - self.element + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_element(self) -> ::Element { + self.element.0 } /// Deserialize from bytes - pub fn deserialize( - bytes: ::Serialization, - ) -> Result, Error> { - ::deserialize(&bytes) - .map(|element| VerifyingKey { element }) - .map_err(|e| e.into()) + pub fn deserialize(bytes: &[u8]) -> Result, Error> { + Ok(Self::new(SerializableElement::deserialize(bytes)?.0)) } /// Serialize `VerifyingKey` to bytes - pub fn serialize(&self) -> ::Serialization { - ::serialize(&self.element) + pub fn serialize(&self) -> Result, Error> { + self.element.serialize() } /// Verify a purported `signature` with a pre-hashed [`Challenge`] made by this verification /// key. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn verify_prehashed( &self, challenge: Challenge, @@ -64,7 +63,7 @@ where // // where h is the cofactor let zB = C::Group::generator() * signature.z; - let cA = self.element * challenge.0; + let cA = self.element.0 * challenge.0; let check = (zB - cA - signature.R) * C::Group::cofactor(); if check == C::Group::identity() { @@ -84,13 +83,13 @@ where pub(crate) fn from_commitment( commitment: &crate::keys::VerifiableSecretSharingCommitment, ) -> Result, Error> { - Ok(VerifyingKey { - element: commitment + Ok(VerifyingKey::new( + commitment .coefficients() .first() .ok_or(Error::IncorrectCommitment)? .value(), - }) + )) } } @@ -100,7 +99,12 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("VerifyingKey") - .field(&hex::encode(self.serialize())) + .field( + &self + .serialize() + .map(hex::encode) + .unwrap_or("".to_string()), + ) .finish() } } @@ -114,31 +118,6 @@ where fn from_hex>(hex: T) -> Result { let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; - match v.try_into() { - Ok(bytes) => Self::deserialize(bytes).map_err(|_| "malformed verifying key encoding"), - Err(_) => Err("malformed verifying key encoding"), - } - } -} - -#[cfg(feature = "serde")] -impl TryFrom> for VerifyingKey -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ElementSerialization) -> Result { - Self::deserialize(value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ElementSerialization -where - C: Ciphersuite, -{ - fn from(value: VerifyingKey) -> Self { - Self(value.serialize()) + Self::deserialize(&v).map_err(|_| "malformed verifying key encoding") } } diff --git a/frost-ed25519/CHANGELOG.md b/frost-ed25519/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-ed25519/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-ed25519/Cargo.toml b/frost-ed25519/Cargo.toml index 4bd685af6..8a073961e 100644 --- a/frost-ed25519/Cargo.toml +++ b/frost-ed25519/Cargo.toml @@ -1,20 +1,13 @@ [package] name = "frost-ed25519" -edition = "2021" -# When releasing to crates.io: -# - Update html_root_url -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = [ - "Deirdre Connolly ", - "Chelsea Komlo ", - "Conrado Gouvea " -] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] +license.workspace = true +repository.workspace = true +categories.workspace = true keywords = ["cryptography", "crypto", "ed25519", "threshold", "signature"] description = "A Schnorr signature scheme over Ed25519 that supports FROST." @@ -23,37 +16,36 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -curve25519-dalek = { version = "=4.1.2", features = ["rand_core"] } -document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } -rand_core = "0.6" -sha2 = "0.10.2" +curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } +document-features.workspace = true +frost-core.workspace = true +frost-rerandomized.workspace = true +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] -criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } -ed25519-dalek = "2.0.0" -insta = { version = "1.31.0", features = ["yaml"] } -hex = "0.4.3" -lazy_static = "1.4" -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +criterion.workspace = true +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +ed25519-dalek = "2.1.0" +insta.workspace = true +hex.workspace = true +lazy_static.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). serde = ["frost-core/serde"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ed25519/README.md b/frost-ed25519/README.md index 6560d5b07..500f8f449 100644 --- a/frost-ed25519/README.md +++ b/frost-ed25519/README.md @@ -1,6 +1,13 @@ An implementation of Schnorr signatures on the Ed25519 curve for both single and threshold numbers of signers (FROST). +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +Ed25519 curve. For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + + + ## Example: key generation with trusted dealer and FROST signing Creating a key with a trusted dealer and splitting into shares; then signing a message @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_ed25519 as frost; -use rand::thread_rng; use std::collections::BTreeMap; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; let (shares, pubkey_package) = frost::keys::generate_with_dealer( @@ -45,14 +51,14 @@ let mut commitments_map = BTreeMap::new(); //////////////////////////////////////////////////////////////////////////// // In practice, each iteration of this loop will be executed by its respective participant. -for participant_index in 1..(min_signers as u16 + 1) { +for participant_index in 1..=min_signers { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _threshold_. # // ANCHOR: round1_commit let (nonces, commitments) = frost::round1::commit( - key_packages[&participant_identifier].signing_share(), + key_package.signing_share(), &mut rng, ); # // ANCHOR_END: round1_commit diff --git a/frost-ed25519/benches/bench.rs b/frost-ed25519/benches/bench.rs index 04cfbfb1a..4317e05b1 100644 --- a/frost-ed25519/benches/bench.rs +++ b/frost-ed25519/benches/bench.rs @@ -1,16 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_ed25519::*; fn bench_ed25519_batch_verify(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_batch_verify::(c, "ed25519", &mut rng); } fn bench_ed25519_sign(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_sign::(c, "ed25519", &mut rng); } diff --git a/frost-ed25519/dkg.md b/frost-ed25519/dkg.md index 797422c74..244d9ba3d 100644 --- a/frost-ed25519/dkg.md +++ b/frost-ed25519/dkg.md @@ -3,35 +3,27 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from -a `u16`. The process in which these identifiers are allocated is up to the application. - -The distributed key generation process has 3 parts, with 2 communication rounds -between them, in which each participant needs to send a "package" to every other -participant. In the first round, each participant sends the same package -(a [`round1::Package`]) to every other. In the second round, each receiver gets -their own package (a [`round2::Package`]). - -Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] -that MUST be kept secret. Between part 2 and 3, each participant needs to hold -onto a [`round2::SecretPackage`]. - -After the third part, each participant will get a [`KeyPackage`] with their -long-term secret share that must be kept secret, and a [`PublicKeyPackage`] -that is public (and will be the same between all participants). With those -they can proceed to sign messages with FROST. - +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). ## Example +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + ```rust # // ANCHOR: dkg_import -use rand::thread_rng; use std::collections::BTreeMap; use frost_ed25519 as frost; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; @@ -48,7 +40,10 @@ let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG protocol. @@ -70,7 +65,10 @@ for participant_index in 1..=max_signers { // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). for receiver_participant_index in 1..=max_signers { if receiver_participant_index == participant_index { continue; @@ -96,7 +94,8 @@ let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG protocol. @@ -118,7 +117,8 @@ for participant_index in 1..=max_signers { // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { diff --git a/frost-ed25519/src/keys/dkg.rs b/frost-ed25519/src/keys/dkg.rs index 082477589..42407bbd3 100644 --- a/frost-ed25519/src/keys/dkg.rs +++ b/frost-ed25519/src/keys/dkg.rs @@ -45,7 +45,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -55,13 +55,21 @@ pub fn part1( frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, @@ -70,14 +78,22 @@ pub fn part2( } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, diff --git a/frost-ed25519/src/keys/refresh.rs b/frost-ed25519/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-ed25519/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-ed25519/src/keys/repairable.rs b/frost-ed25519/src/keys/repairable.rs index deb5a8334..9e809ed30 100644 --- a/frost-ed25519/src/keys/repairable.rs +++ b/frost-ed25519/src/keys/repairable.rs @@ -4,61 +4,67 @@ //! The RTS is used to help a signer (participant) repair their lost share. This is achieved //! using a subset of the other signers know here as `helpers`. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; +use crate::keys::{KeyPackage, PublicKeyPackage}; // This is imported separately to make `gencode` work. // (if it were below, the position of the import would vary between ciphersuites // after `cargo fmt`) -use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; use crate::{Ed25519Sha512, Error}; -use super::{SecretShare, VerifiableSecretSharingCommitment}; +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; -/// Step 1 of RTS. +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Error> { - frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) } -/// Step 2 of RTS. -/// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// Part 2 of RTS. /// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { - frost::keys::repairable::repair_share_step_2::(deltas_j) +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) } -/// Step 3 of RTS +/// Part 3 of RTS. /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { - frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) } #[cfg(test)] mod tests { use lazy_static::lazy_static; - use rand::thread_rng; + use serde_json::Value; use crate::Ed25519Sha512; @@ -70,30 +76,30 @@ mod tests { } #[test] - fn check_repair_share_step_1() { - let rng = thread_rng(); + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; - frost_core::tests::repairable::check_repair_share_step_1::(rng); + frost_core::tests::repairable::check_repair_share_part1::(rng); } #[test] - fn check_repair_share_step_2() { - frost_core::tests::repairable::check_repair_share_step_2::(&REPAIR_SHARE); + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::(&REPAIR_SHARE); } #[test] - fn check_repair_share_step_3() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_3::( + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( rng, &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_1_fails_with_invalid_min_signers() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< Ed25519Sha512, _, >(rng); diff --git a/frost-ed25519/src/lib.rs b/frost-ed25519/src/lib.rs index b774c17d8..cb7a818bf 100644 --- a/frost-ed25519/src/lib.rs +++ b/frost-ed25519/src/lib.rs @@ -1,11 +1,13 @@ +#![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc = document_features::document_features!()] -use std::collections::BTreeMap; +extern crate alloc; + +use alloc::collections::BTreeMap; use curve25519_dalek::{ constants::ED25519_BASEPOINT_POINT, @@ -23,7 +25,9 @@ use frost_core as frost; mod tests; // Re-exports in our public API -pub use frost_core::{serde, Ciphersuite, Field, FieldError, Group, GroupError}; +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; pub use rand_core; /// An error. @@ -99,8 +103,11 @@ impl Group for Ed25519Group { ED25519_BASEPOINT_POINT } - fn serialize(element: &Self::Element) -> Self::Serialization { - element.compress().to_bytes() + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + Ok(element.compress().to_bytes()) } fn deserialize(buf: &Self::Serialization) -> Result { @@ -137,7 +144,7 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 64] { h.update(i); } let mut output = [0u8; 64]; - output.copy_from_slice(h.finalize().as_slice()); + output.copy_from_slice(h.finalize().as_ref()); output } @@ -148,7 +155,7 @@ fn hash_to_scalar(inputs: &[&[u8]]) -> Scalar { /// Context string from the ciphersuite in the [spec] /// -/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-1 +/// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-1 const CONTEXT_STRING: &str = "FROST-ED25519-SHA512-v1"; /// An implementation of the FROST(Ed25519, SHA-512) ciphersuite. @@ -166,35 +173,35 @@ impl Ciphersuite for Ed25519Sha512 { /// H1 for FROST(Ed25519, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-2.2.2.1 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho", m]) } /// H2 for FROST(Ed25519, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-2.2.2.2 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[m]) } /// H3 for FROST(Ed25519, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-2.2.2.3 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce", m]) } /// H4 for FROST(Ed25519, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-2.2.2.4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-2.4.2.8 fn H4(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) } /// H5 for FROST(Ed25519, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.1-2.2.2.5 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.1-2.4.2.10 fn H5(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) } @@ -227,8 +234,6 @@ pub type Identifier = frost::Identifier; /// FROST(Ed25519, SHA-512) keys, key generation, key shares. pub mod keys { - use std::collections::BTreeMap; - use super::*; /// The identifier list to use when generating key shares. @@ -320,6 +325,7 @@ pub mod keys { pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; pub mod dkg; + pub mod refresh; pub mod repairable; } @@ -412,6 +418,25 @@ pub fn aggregate( frost::aggregate(signing_package, signature_shares, pubkeys) } +/// The type of cheater detection to use. +pub type CheaterDetection = frost::CheaterDetection; + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result { + frost::aggregate_custom( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + ) +} + /// A signing key for a Schnorr signature on FROST(Ed25519, SHA-512). pub type SigningKey = frost_core::SigningKey; diff --git a/frost-ed25519/src/rerandomized.rs b/frost-ed25519/src/rerandomized.rs new file mode 100644 index 000000000..7a90a0dd1 --- /dev/null +++ b/frost-ed25519/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-ed25519/src/tests/batch.rs b/frost-ed25519/src/tests/batch.rs index 26497935c..a57301f01 100644 --- a/frost-ed25519/src/tests/batch.rs +++ b/frost-ed25519/src/tests/batch.rs @@ -1,24 +1,22 @@ -use rand::thread_rng; - use crate::*; #[test] fn check_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::batch_verify::(rng); } #[test] fn check_bad_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::bad_batch_verify::(rng); } #[test] fn empty_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::empty_batch_verify::(rng); } diff --git a/frost-ed25519/src/tests/coefficient_commitment.rs b/frost-ed25519/src/tests/coefficient_commitment.rs index 113eb1c7c..d45994e83 100644 --- a/frost-ed25519/src/tests/coefficient_commitment.rs +++ b/frost-ed25519/src/tests/coefficient_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,7 +12,7 @@ lazy_static! { #[test] fn check_serialization_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< Ed25519Sha512, _, @@ -22,7 +21,7 @@ fn check_serialization_of_coefficient_commitment() { #[test] fn check_create_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< Ed25519Sha512, _, @@ -37,7 +36,7 @@ fn check_create_coefficient_commitment_error() { #[test] fn check_get_value_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< Ed25519Sha512, diff --git a/frost-ed25519/src/tests/vss_commitment.rs b/frost-ed25519/src/tests/vss_commitment.rs index 2bd1bae10..3e0fcd26e 100644 --- a/frost-ed25519/src/tests/vss_commitment.rs +++ b/frost-ed25519/src/tests/vss_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,26 +12,51 @@ lazy_static! { #[test] fn check_serialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); } +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment::(rng); } +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment_error() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( rng, &ELEMENTS, ); } +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::< + Ed25519Sha512, + _, + >(rng, &ELEMENTS); +} + #[test] fn check_compute_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_compute_public_key_package::(rng); } diff --git a/frost-ed25519/tests/common_traits_tests.rs b/frost-ed25519/tests/common_traits_tests.rs index 933630274..db173929b 100644 --- a/frost-ed25519/tests/common_traits_tests.rs +++ b/frost-ed25519/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_ed25519::SigningKey; use helpers::samples; -use rand::thread_rng; #[allow(clippy::unnecessary_literal_unwrap)] fn check_common_traits_for_type(v: T) { @@ -20,7 +19,7 @@ fn check_common_traits_for_type(v: #[test] fn check_signing_key_common_traits() { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; let signing_key = SigningKey::new(&mut rng); check_common_traits_for_type(signing_key); } diff --git a/frost-ed25519/tests/helpers/mod.rs b/frost-ed25519/tests/helpers/mod.rs index 8f0f795dc..2a936ac3e 100644 --- a/frost-ed25519/tests/helpers/mod.rs +++ b/frost-ed25519/tests/helpers/mod.rs @@ -14,11 +14,11 @@ pub fn verify_signature( group_pubkey: frost_core::VerifyingKey, ) { let sig = { - let bytes: [u8; 64] = group_signature.serialize(); + let bytes: [u8; 64] = group_signature.serialize().unwrap().try_into().unwrap(); ed25519_dalek::Signature::from(bytes) }; let pub_key = { - let bytes = group_pubkey.serialize(); + let bytes = group_pubkey.serialize().unwrap().try_into().unwrap(); ed25519_dalek::VerifyingKey::from_bytes(&bytes).unwrap() }; // Check that signature validation has the expected result. diff --git a/frost-ed25519/tests/helpers/samples.json b/frost-ed25519/tests/helpers/samples.json index 3402fbe74..a61e0c4b8 100644 --- a/frost-ed25519/tests/helpers/samples.json +++ b/frost-ed25519/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "5866666666666666666666666666666666666666666666666666666666666666498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "5866666666666666666666666666666666666666666666666666666666666666", "element2": "c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ed25519/tests/helpers/samples.rs b/frost-ed25519/tests/helpers/samples.rs index 3080df015..2c6c9c531 100644 --- a/frost-ed25519/tests/helpers/samples.rs +++ b/frost-ed25519/tests/helpers/samples.rs @@ -36,18 +36,20 @@ fn scalar1() -> Scalar { pub fn signing_nonces() -> SigningNonces { let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); - let hiding_nonce = Nonce::deserialize(serialized_scalar1).unwrap(); - let binding_nonce = Nonce::deserialize(serialized_scalar2).unwrap(); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); SigningNonces::from_nonces(hiding_nonce, binding_nonce) } /// Generate a sample SigningCommitments. pub fn signing_commitments() -> SigningCommitments { - let serialized_element1 = ::Group::serialize(&element1()); - let serialized_element2 = ::Group::serialize(&element2()); - let hiding_nonce_commitment = NonceCommitment::deserialize(serialized_element1).unwrap(); - let binding_nonce_commitment = NonceCommitment::deserialize(serialized_element2).unwrap(); + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) } @@ -65,15 +67,15 @@ pub fn signing_package() -> SigningPackage { pub fn signature_share() -> SignatureShare { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - SignatureShare::deserialize(serialized_scalar).unwrap() + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() } /// Generate a sample SecretShare. pub fn secret_share() -> SecretShare { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); @@ -84,11 +86,11 @@ pub fn secret_share() -> SecretShare { pub fn key_package() -> KeyPackage { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) } @@ -96,38 +98,82 @@ pub fn key_package() -> KeyPackage { /// Generate a sample PublicKeyPackage. pub fn public_key_package() -> PublicKeyPackage { let identifier = 42u16.try_into().unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) } /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>() - .try_into() - .unwrap(); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + /// Generate a sample round2::Package. pub fn round2_package() -> round2::Package { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); round2::Package::new(signing_share) } diff --git a/frost-ed25519/tests/helpers/vectors_dkg.json b/frost-ed25519/tests/helpers/vectors_dkg.json index bcad1c9c2..4284fe4af 100644 --- a/frost-ed25519/tests/helpers/vectors_dkg.json +++ b/frost-ed25519/tests/helpers/vectors_dkg.json @@ -35,7 +35,7 @@ "3": "7fde55b354d5d8dddc940fe932de5d1a6110b9bc4edeba2db7b32c34074d3a0a" }, "verifying_share": "f326b756ed38b43a94bdac698e044d9e3f3a08a40e7c9d2e5346dd5bfaadf2f5", - "signing_share": "80a16ad6cf78be27995d5e312722ff2d42a5b08380b49dbc2e7e65c124220e0a" + "signing_share": "b9ed88cb30a9ddbc00dbdf7c40d6c76f791989cf9f3119bad899287c5f6c2303" }, "3": { "identifier": 3, @@ -49,7 +49,7 @@ "2": "6b3e57fab1e74e61aacfd93d4926172a7e878a48540fe97367721e38485b3a00" }, "verifying_share": "6bc91a2755902d955ce220ad0df6fbf57162260949d40bcf5a69cfffec9c085a", - "signing_share": "8bd411475cdc423c1ba94cf11b80e776592d5f675419398800d17173d673f40a" + "signing_share": "35b2b8fad8352f6e6a7c8fee082f45a339a37f0085c1f662c44c3f10903d970d" } } } diff --git a/frost-ed25519/tests/integration_tests.rs b/frost-ed25519/tests/integration_tests.rs index 1421079a4..2cc6620a6 100644 --- a/frost-ed25519/tests/integration_tests.rs +++ b/frost-ed25519/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_ed25519::*; use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; #[test] @@ -10,14 +9,14 @@ fn check_zero_key_fails() { #[test] fn check_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); } #[test] fn check_dkg_part1_fails_with_invalid_signers_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -31,7 +30,7 @@ fn check_dkg_part1_fails_with_invalid_signers_min_signers() { #[test] fn check_dkg_part1_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -45,7 +44,7 @@ fn check_dkg_part1_fails_with_min_signers_greater_than_max() { #[test] fn check_dkg_part1_fails_with_invalid_signers_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -59,21 +58,80 @@ fn check_dkg_part1_fails_with_invalid_signers_max_signers() { #[test] fn check_rts() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( + rng, + ); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Ed25519Sha512, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Ed25519Sha512, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::( + rng, + ); +} + #[test] fn check_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); } #[test] fn check_sign_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -87,7 +145,7 @@ fn check_sign_with_dealer_fails_with_invalid_min_signers() { #[test] fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -101,7 +159,7 @@ fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { #[test] fn check_sign_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -117,13 +175,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_ed25519_sha512() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_share_generation::(rng); } #[test] fn check_share_generation_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 0; let max_signers = 3; @@ -137,7 +195,7 @@ fn check_share_generation_fails_with_invalid_min_signers() { #[test] fn check_share_generation_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -151,7 +209,7 @@ fn check_share_generation_fails_with_min_signers_greater_than_max() { #[test] fn check_share_generation_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 0; @@ -215,7 +273,7 @@ fn check_identifier_generation() -> Result<(), Error> { #[test] fn check_sign_with_dealer_and_identifiers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< Ed25519Sha512, @@ -225,7 +283,7 @@ fn check_sign_with_dealer_and_identifiers() { #[test] fn check_sign_with_missing_identifier() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::( rng, ); @@ -233,8 +291,18 @@ fn check_sign_with_missing_identifier() { #[test] fn check_sign_with_incorrect_commitments() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::( rng, ); } + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng).await; + }) + .await + .unwrap(); +} diff --git a/frost-ed25519/tests/interoperability_tests.rs b/frost-ed25519/tests/interoperability_tests.rs index 9c27193fb..e758ee248 100644 --- a/frost-ed25519/tests/interoperability_tests.rs +++ b/frost-ed25519/tests/interoperability_tests.rs @@ -1,21 +1,18 @@ use crate::Ed25519Sha512; use frost_ed25519::*; -use rand::thread_rng; mod helpers; #[test] fn check_interoperability_in_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; // Test with multiple keys/signatures to better exercise the key generation // and the interoperability check. A smaller number of iterations is used // because DKG takes longer and otherwise the test would be too slow. for _ in 0..32 { let (msg, group_signature, group_pubkey) = - frost_core::tests::ciphersuite_generic::check_sign_with_dkg::( - rng.clone(), - ); + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); helpers::verify_signature(&msg, group_signature, group_pubkey); } @@ -23,15 +20,13 @@ fn check_interoperability_in_sign_with_dkg() { #[test] fn check_interoperability_in_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; // Test with multiple keys/signatures to better exercise the key generation // and the interoperability check. for _ in 0..256 { let (msg, group_signature, group_pubkey) = - frost_core::tests::ciphersuite_generic::check_sign_with_dealer::( - rng.clone(), - ); + frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); // Check that the threshold signature can be verified by the `ed25519_dalek` crate // public key (interoperability test) diff --git a/frost-ed25519/tests/recreation_tests.rs b/frost-ed25519/tests/recreation_tests.rs index 7fa11b792..479b7d93f 100644 --- a/frost-ed25519/tests/recreation_tests.rs +++ b/frost-ed25519/tests/recreation_tests.rs @@ -54,7 +54,7 @@ fn check_signature_share_recreation() { let encoded = signature_share.serialize(); - let new_signature_share = SignatureShare::deserialize(encoded).unwrap(); + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); assert!(signature_share == new_signature_share); } @@ -101,12 +101,51 @@ fn check_public_key_package_recreation() { let verifying_shares = public_key_package.verifying_shares(); let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); - let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); assert!(public_key_package == new_public_key_package); } +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + /// Check if round1::Package can be recreated. #[test] fn check_round1_package_recreation() { @@ -120,6 +159,28 @@ fn check_round1_package_recreation() { assert!(round1_package == new_round1_package); } +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + /// Check if round2::Package can be recreated. #[test] fn check_round2_package_recreation() { diff --git a/frost-ed25519/tests/rerandomized_tests.rs b/frost-ed25519/tests/rerandomized_tests.rs index e6981bdd9..121738744 100644 --- a/frost-ed25519/tests/rerandomized_tests.rs +++ b/frost-ed25519/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_ed25519::Ed25519Sha512; -use rand::thread_rng; #[test] fn check_randomized_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let (_msg, _group_signature, _group_pubkey) = frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); diff --git a/frost-ed25519/tests/serde_tests.rs b/frost-ed25519/tests/serde_tests.rs index 9f722797b..a36c278b9 100644 --- a/frost-ed25519/tests/serde_tests.rs +++ b/frost-ed25519/tests/serde_tests.rs @@ -434,7 +434,7 @@ fn check_key_package_serialization() { #[test] fn check_public_key_package_serialization() { - let public_key_package = samples::public_key_package(); + let public_key_package = samples::public_key_package_new(); let json = serde_json::to_string_pretty(&public_key_package).unwrap(); println!("{}", json); @@ -450,11 +450,27 @@ fn check_public_key_package_serialization() { "verifying_shares": { "2a00000000000000000000000000000000000000000000000000000000000000": "5866666666666666666666666666666666666666666666666666666666666666" }, - "verifying_key": "5866666666666666666666666666666666666666666666666666666666666666" + "verifying_key": "5866666666666666666666666666666666666666666666666666666666666666", + "min_signers": 2 }"#; let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); assert!(public_key_package == decoded_public_key_package); + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-ED25519-SHA512-v1" + }, + "verifying_shares": { + "2a00000000000000000000000000000000000000000000000000000000000000": "5866666666666666666666666666666666666666666666666666666666666666" + }, + "verifying_key": "5866666666666666666666666666666666666666666666666666666666666666" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + let invalid_json = "{}"; assert!(serde_json::from_str::(invalid_json).is_err()); diff --git a/frost-ed25519/tests/serialization_tests.rs b/frost-ed25519/tests/serialization_tests.rs index fbe26a41f..6af901684 100644 --- a/frost-ed25519/tests/serialization_tests.rs +++ b/frost-ed25519/tests/serialization_tests.rs @@ -49,8 +49,11 @@ fn check_signing_package_postcard_serialization() { fn check_signature_share_postcard_serialization() { let signature_share = samples::signature_share(); let bytes = signature_share.serialize(); - assert_snapshot!(hex::encode(bytes)); - assert_eq!(signature_share, SignatureShare::deserialize(bytes).unwrap()); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); } #[test] fn check_secret_share_postcard_serialization() { @@ -79,6 +82,28 @@ fn check_public_key_package_postcard_serialization() { ); } +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round1_package_postcard_serialization() { let round1_package = samples::round1_package(); @@ -90,6 +115,17 @@ fn check_round1_package_postcard_serialization() { ); } +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round2_package_postcard_serialization() { let round2_package = samples::round2_package(); diff --git a/frost-ed25519/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-ed25519/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..342de36f3 --- /dev/null +++ b/frost-ed25519/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed25519/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00b169f0da012a00000000000000000000000000000000000000000000000000000000000000586666666666666666666666666666666666666666666666666666666666666658666666666666666666666666666666666666666666666666666666666666660102 diff --git a/frost-ed25519/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-ed25519/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..66e213f81 --- /dev/null +++ b/frost-ed25519/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed25519/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a0000000000000000000000000000000000000000000000000000000000000002498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a0158666666666666666666666666666666666666666666666666666666666666660203 diff --git a/frost-ed25519/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-ed25519/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..e7d9f23af --- /dev/null +++ b/frost-ed25519/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed25519/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a00000000000000000000000000000000000000000000000000000000000000015866666666666666666666666666666666666666666666666666666666666666498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a0203 diff --git a/frost-ed448/CHANGELOG.md b/frost-ed448/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-ed448/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-ed448/Cargo.toml b/frost-ed448/Cargo.toml index dcae381d2..5c26e30ee 100644 --- a/frost-ed448/Cargo.toml +++ b/frost-ed448/Cargo.toml @@ -1,19 +1,13 @@ [package] name = "frost-ed448" -edition = "2021" -# When releasing to crates.io: -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = [ - "Deirdre Connolly ", - "Chelsea Komlo ", - "Conrado Gouvea " -] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] +license.workspace = true +repository.workspace = true +categories.workspace = true keywords = ["cryptography", "crypto", "ed448", "threshold", "signature"] description = "A Schnorr signature scheme over Ed448 that supports FROST." @@ -22,36 +16,35 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -document-features = "0.2.7" +document-features.workspace = true ed448-goldilocks = { version = "0.9.0" } -frost-core = { path = "../frost-core", version = "1.0.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } -rand_core = "0.6" -sha3 = "0.10.6" +frost-core.workspace = true +frost-rerandomized.workspace = true +rand_core.workspace = true +sha3 = { version = "0.10.6", default-features = false } [dev-dependencies] -criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } -lazy_static = "1.4" -insta = { version = "1.31.0", features = ["yaml"] } -hex = "0.4.3" -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +criterion.workspace = true +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +lazy_static.workspace = true +insta.workspace = true +hex.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). serde = ["frost-core/serde"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ed448/README.md b/frost-ed448/README.md index 74d8fa6dc..77f1eb8c7 100644 --- a/frost-ed448/README.md +++ b/frost-ed448/README.md @@ -1,6 +1,13 @@ An implementation of Schnorr signatures on the Ed448 curve for both single and threshold numbers of signers (FROST). +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +Ed448 curve. For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + + + ## Example: key generation with trusted dealer and FROST signing Creating a key with a trusted dealer and splitting into shares; then signing a message @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_ed448 as frost; -use rand::thread_rng; use std::collections::BTreeMap; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; let (shares, pubkey_package) = frost::keys::generate_with_dealer( @@ -45,14 +51,14 @@ let mut commitments_map = BTreeMap::new(); //////////////////////////////////////////////////////////////////////////// // In practice, each iteration of this loop will be executed by its respective participant. -for participant_index in 1..(min_signers as u16 + 1) { +for participant_index in 1..=min_signers { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _threshold_. # // ANCHOR: round1_commit let (nonces, commitments) = frost::round1::commit( - key_packages[&participant_identifier].signing_share(), + key_package.signing_share(), &mut rng, ); # // ANCHOR_END: round1_commit diff --git a/frost-ed448/benches/bench.rs b/frost-ed448/benches/bench.rs index 343f72f25..ba40feb33 100644 --- a/frost-ed448/benches/bench.rs +++ b/frost-ed448/benches/bench.rs @@ -1,18 +1,17 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_ed448::*; // bench_ed448_batch_verify not included until batch verification is fixed for Ed448 #[allow(unused)] fn bench_ed448_batch_verify(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_batch_verify::(c, "ed448", &mut rng); } fn bench_ed448_sign(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_sign::(c, "ed448", &mut rng); } diff --git a/frost-ed448/dkg.md b/frost-ed448/dkg.md index 2d60b14d6..ff304b959 100644 --- a/frost-ed448/dkg.md +++ b/frost-ed448/dkg.md @@ -3,35 +3,27 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from -a `u16`. The process in which these identifiers are allocated is up to the application. - -The distributed key generation process has 3 parts, with 2 communication rounds -between them, in which each participant needs to send a "package" to every other -participant. In the first round, each participant sends the same package -(a [`round1::Package`]) to every other. In the second round, each receiver gets -their own package (a [`round2::Package`]). - -Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] -that MUST be kept secret. Between part 2 and 3, each participant needs to hold -onto a [`round2::SecretPackage`]. - -After the third part, each participant will get a [`KeyPackage`] with their -long-term secret share that must be kept secret, and a [`PublicKeyPackage`] -that is public (and will be the same between all participants). With those -they can proceed to sign messages with FROST. - +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). ## Example +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + ```rust # // ANCHOR: dkg_import -use rand::thread_rng; use std::collections::BTreeMap; use frost_ed448 as frost; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; @@ -48,7 +40,10 @@ let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG protocol. @@ -70,7 +65,10 @@ for participant_index in 1..=max_signers { // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). for receiver_participant_index in 1..=max_signers { if receiver_participant_index == participant_index { continue; @@ -96,7 +94,8 @@ let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG protocol. @@ -118,7 +117,8 @@ for participant_index in 1..=max_signers { // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { diff --git a/frost-ed448/src/keys/dkg.rs b/frost-ed448/src/keys/dkg.rs index 082477589..42407bbd3 100644 --- a/frost-ed448/src/keys/dkg.rs +++ b/frost-ed448/src/keys/dkg.rs @@ -45,7 +45,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -55,13 +55,21 @@ pub fn part1( frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, @@ -70,14 +78,22 @@ pub fn part2( } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, diff --git a/frost-ed448/src/keys/refresh.rs b/frost-ed448/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-ed448/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-ed448/src/keys/repairable.rs b/frost-ed448/src/keys/repairable.rs index b44709fcb..f1e21d479 100644 --- a/frost-ed448/src/keys/repairable.rs +++ b/frost-ed448/src/keys/repairable.rs @@ -4,61 +4,67 @@ //! The RTS is used to help a signer (participant) repair their lost share. This is achieved //! using a subset of the other signers know here as `helpers`. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; +use crate::keys::{KeyPackage, PublicKeyPackage}; // This is imported separately to make `gencode` work. // (if it were below, the position of the import would vary between ciphersuites // after `cargo fmt`) -use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; use crate::{Ed448Shake256, Error}; -use super::{SecretShare, VerifiableSecretSharingCommitment}; +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; -/// Step 1 of RTS. +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Error> { - frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) } -/// Step 2 of RTS. -/// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// Part 2 of RTS. /// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { - frost::keys::repairable::repair_share_step_2::(deltas_j) +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) } -/// Step 3 of RTS +/// Part 3 of RTS. /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { - frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) } #[cfg(test)] mod tests { use lazy_static::lazy_static; - use rand::thread_rng; + use serde_json::Value; use crate::Ed448Shake256; @@ -70,30 +76,30 @@ mod tests { } #[test] - fn check_repair_share_step_1() { - let rng = thread_rng(); + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; - frost_core::tests::repairable::check_repair_share_step_1::(rng); + frost_core::tests::repairable::check_repair_share_part1::(rng); } #[test] - fn check_repair_share_step_2() { - frost_core::tests::repairable::check_repair_share_step_2::(&REPAIR_SHARE); + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::(&REPAIR_SHARE); } #[test] - fn check_repair_share_step_3() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_3::( + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( rng, &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_1_fails_with_invalid_min_signers() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< Ed448Shake256, _, >(rng); diff --git a/frost-ed448/src/lib.rs b/frost-ed448/src/lib.rs index a73a1de1f..154f445f6 100644 --- a/frost-ed448/src/lib.rs +++ b/frost-ed448/src/lib.rs @@ -1,11 +1,12 @@ #![allow(non_snake_case)] #![deny(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc = document_features::document_features!()] -use std::collections::BTreeMap; +extern crate alloc; + +use alloc::collections::BTreeMap; use ed448_goldilocks::{ curve::{edwards::CompressedEdwardsY, ExtendedPoint}, @@ -24,7 +25,9 @@ use frost_core as frost; mod tests; // Re-exports in our public API -pub use frost_core::{serde, Ciphersuite, Field, FieldError, Group, GroupError}; +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; pub use rand_core; /// An error. @@ -98,8 +101,11 @@ impl Group for Ed448Group { Self::Element::generator() } - fn serialize(element: &Self::Element) -> Self::Serialization { - element.compress().0 + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + Ok(element.compress().0) } fn deserialize(buf: &Self::Serialization) -> Result { @@ -143,7 +149,7 @@ fn hash_to_scalar(inputs: &[&[u8]]) -> Scalar { /// Context string from the ciphersuite in the [spec] /// -/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-1 +/// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-1 const CONTEXT_STRING: &str = "FROST-ED448-SHAKE256-v1"; /// An implementation of the FROST(Ed448, SHAKE256) ciphersuite. @@ -161,35 +167,35 @@ impl Ciphersuite for Ed448Shake256 { /// H1 for FROST(Ed448, SHAKE256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.1 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho", m]) } /// H2 for FROST(Ed448, SHAKE256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.2 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[b"SigEd448\0\0", m]) } /// H3 for FROST(Ed448, SHAKE256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.3 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce", m]) } /// H4 for FROST(Ed448, SHAKE256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-2.4.2.8 fn H4(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) } /// H5 for FROST(Ed448, SHAKE256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.5 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.3-2.4.2.10 fn H5(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) } @@ -223,7 +229,6 @@ pub type Identifier = frost::Identifier; /// FROST(Ed448, SHAKE256) keys, key generation, key shares. pub mod keys { use super::*; - use std::collections::BTreeMap; /// The identifier list to use when generating key shares. pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, E>; @@ -314,6 +319,7 @@ pub mod keys { pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; pub mod dkg; + pub mod refresh; pub mod repairable; } @@ -406,6 +412,25 @@ pub fn aggregate( frost::aggregate(signing_package, signature_shares, pubkeys) } +/// The type of cheater detection to use. +pub type CheaterDetection = frost::CheaterDetection; + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result { + frost::aggregate_custom( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + ) +} + /// A signing key for a Schnorr signature on FROST(Ed448, SHAKE256). pub type SigningKey = frost_core::SigningKey; diff --git a/frost-ed448/src/rerandomized.rs b/frost-ed448/src/rerandomized.rs new file mode 100644 index 000000000..10d5d013c --- /dev/null +++ b/frost-ed448/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-ed448/src/tests/batch.rs b/frost-ed448/src/tests/batch.rs index 85b6b1a6c..5c84b5e50 100644 --- a/frost-ed448/src/tests/batch.rs +++ b/frost-ed448/src/tests/batch.rs @@ -1,24 +1,22 @@ -use rand::thread_rng; - use crate::*; #[test] fn check_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::batch_verify::(rng); } #[test] fn check_bad_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::bad_batch_verify::(rng); } #[test] fn empty_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::empty_batch_verify::(rng); } diff --git a/frost-ed448/src/tests/coefficient_commitment.rs b/frost-ed448/src/tests/coefficient_commitment.rs index d088ad15b..0d2091aa6 100644 --- a/frost-ed448/src/tests/coefficient_commitment.rs +++ b/frost-ed448/src/tests/coefficient_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,7 +12,7 @@ lazy_static! { #[test] fn check_serialization_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< Ed448Shake256, _, @@ -22,7 +21,7 @@ fn check_serialization_of_coefficient_commitment() { #[test] fn check_create_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< Ed448Shake256, _, @@ -37,7 +36,7 @@ fn check_create_coefficient_commitment_error() { #[test] fn check_get_value_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< Ed448Shake256, diff --git a/frost-ed448/src/tests/vss_commitment.rs b/frost-ed448/src/tests/vss_commitment.rs index 98810b382..7e1f83441 100644 --- a/frost-ed448/src/tests/vss_commitment.rs +++ b/frost-ed448/src/tests/vss_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,26 +12,51 @@ lazy_static! { #[test] fn check_serialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); } +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment::(rng); } +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment_error() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( rng, &ELEMENTS, ); } +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::< + Ed448Shake256, + _, + >(rng, &ELEMENTS); +} + #[test] fn check_compute_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_compute_public_key_package::(rng); } diff --git a/frost-ed448/tests/common_traits_tests.rs b/frost-ed448/tests/common_traits_tests.rs index 44f389ad5..749cc7033 100644 --- a/frost-ed448/tests/common_traits_tests.rs +++ b/frost-ed448/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_ed448::SigningKey; use helpers::samples; -use rand::thread_rng; #[allow(clippy::unnecessary_literal_unwrap)] fn check_common_traits_for_type(v: T) { @@ -20,7 +19,7 @@ fn check_common_traits_for_type(v: #[test] fn check_signing_key_common_traits() { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; let signing_key = SigningKey::new(&mut rng); check_common_traits_for_type(signing_key); } diff --git a/frost-ed448/tests/helpers/samples.json b/frost-ed448/tests/helpers/samples.json index 36e862866..f93c3e9ce 100644 --- a/frost-ed448/tests/helpers/samples.json +++ b/frost-ed448/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69004d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00", "element1": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900", "element2": "ed8693eacdfbeada6ba0cdd1beb2bcbb98302a3a8365650db8c4d88a726de3b7d74d8835a0d76e03b0c2865020d659b38d04d74a63e905ae80", "scalar1": "4d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00" -} \ No newline at end of file +} diff --git a/frost-ed448/tests/helpers/samples.rs b/frost-ed448/tests/helpers/samples.rs index 435196f44..529633b88 100644 --- a/frost-ed448/tests/helpers/samples.rs +++ b/frost-ed448/tests/helpers/samples.rs @@ -36,18 +36,20 @@ fn scalar1() -> Scalar { pub fn signing_nonces() -> SigningNonces { let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); - let hiding_nonce = Nonce::deserialize(serialized_scalar1).unwrap(); - let binding_nonce = Nonce::deserialize(serialized_scalar2).unwrap(); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); SigningNonces::from_nonces(hiding_nonce, binding_nonce) } /// Generate a sample SigningCommitments. pub fn signing_commitments() -> SigningCommitments { - let serialized_element1 = ::Group::serialize(&element1()); - let serialized_element2 = ::Group::serialize(&element2()); - let hiding_nonce_commitment = NonceCommitment::deserialize(serialized_element1).unwrap(); - let binding_nonce_commitment = NonceCommitment::deserialize(serialized_element2).unwrap(); + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) } @@ -65,15 +67,15 @@ pub fn signing_package() -> SigningPackage { pub fn signature_share() -> SignatureShare { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - SignatureShare::deserialize(serialized_scalar).unwrap() + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() } /// Generate a sample SecretShare. pub fn secret_share() -> SecretShare { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); @@ -84,11 +86,11 @@ pub fn secret_share() -> SecretShare { pub fn key_package() -> KeyPackage { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) } @@ -96,38 +98,82 @@ pub fn key_package() -> KeyPackage { /// Generate a sample PublicKeyPackage. pub fn public_key_package() -> PublicKeyPackage { let identifier = 42u16.try_into().unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) } /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>() - .try_into() - .unwrap(); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + /// Generate a sample round2::Package. pub fn round2_package() -> round2::Package { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); round2::Package::new(signing_share) } diff --git a/frost-ed448/tests/helpers/vectors_dkg.json b/frost-ed448/tests/helpers/vectors_dkg.json index a0943e87c..09a2297f5 100644 --- a/frost-ed448/tests/helpers/vectors_dkg.json +++ b/frost-ed448/tests/helpers/vectors_dkg.json @@ -42,7 +42,7 @@ "proof_of_knowledge": "eb47491f2461792114d357d02102c1a806451cfa88f1297f7a671a87a04de0ffde478ec1c2b91e743379254fe84eb2e0d170c69aec88bc1980bf8009ffc93d6ee0c3713681aa303cf85595bd975953318ab07be9e56dc6ba22465793ee337e383562fafc7525c05b36732b93f5f4fee22300", "signing_shares": { "1": "77c1831adb90cd94d0404ec2a8730af57d4e29e10ad02e26ee61328c95f1258a17bad98617632d92ee5aa8224793231db3a599d322b82d2300", - "2": "e29c8b642abfa741710945aedadf34ac73ef6863c3e56d599cc3c58039d45b7382674cbd2c8e064c8bae33851c9166536181b83fe34ce02200" + "2": "6aa7e48f3d7f4de3b1a0bb95a5085705e5618b8f67d89cf43dd8d649057d295ff0c837e4a918634a94a773606ed156c9db543c3bcd1e2a0e00" }, "verifying_share": "3a1b5a9945fc64b088174c34e16dbced81f824fe8f9f12d1ec98afd4ea593a6ec75a74f70b77522c66681bd468080b525963dbcc2785d53a00", "signing_share": "da7c3a1048da9e6b8e480a72d48479ab4724f9804e96a44c7f7e04691ffbfdd7a56c2a9644e1ad1075baeae746ce8317f63104e87363652900" diff --git a/frost-ed448/tests/integration_tests.rs b/frost-ed448/tests/integration_tests.rs index 3409a7e02..28bd22ad2 100644 --- a/frost-ed448/tests/integration_tests.rs +++ b/frost-ed448/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_ed448::*; use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; #[test] @@ -10,14 +9,14 @@ fn check_zero_key_fails() { #[test] fn check_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); } #[test] fn check_dkg_part1_fails_with_invalid_signers_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -31,7 +30,7 @@ fn check_dkg_part1_fails_with_invalid_signers_min_signers() { #[test] fn check_dkg_part1_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -45,7 +44,7 @@ fn check_dkg_part1_fails_with_min_signers_greater_than_max() { #[test] fn check_dkg_part1_fails_with_invalid_signers_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -59,21 +58,80 @@ fn check_dkg_part1_fails_with_invalid_signers_max_signers() { #[test] fn check_rts() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( + rng, + ); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Ed448Shake256, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Ed448Shake256, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::( + rng, + ); +} + #[test] fn check_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); } #[test] fn check_sign_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -87,7 +145,7 @@ fn check_sign_with_dealer_fails_with_invalid_min_signers() { #[test] fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -101,7 +159,7 @@ fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { #[test] fn check_sign_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -117,13 +175,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_ed448_shake256() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_share_generation::(rng); } #[test] fn check_share_generation_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 0; let max_signers = 3; @@ -137,7 +195,7 @@ fn check_share_generation_fails_with_invalid_min_signers() { #[test] fn check_share_generation_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -151,7 +209,7 @@ fn check_share_generation_fails_with_min_signers_greater_than_max() { #[test] fn check_share_generation_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 0; @@ -215,7 +273,7 @@ fn check_identifier_generation() -> Result<(), Error> { #[test] fn check_sign_with_dealer_and_identifiers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< Ed448Shake256, @@ -225,7 +283,7 @@ fn check_sign_with_dealer_and_identifiers() { #[test] fn check_sign_with_missing_identifier() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::( rng, ); @@ -233,8 +291,18 @@ fn check_sign_with_missing_identifier() { #[test] fn check_sign_with_incorrect_commitments() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::( rng, ); } + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng).await; + }) + .await + .unwrap(); +} diff --git a/frost-ed448/tests/recreation_tests.rs b/frost-ed448/tests/recreation_tests.rs index 1caababa3..e206ccf08 100644 --- a/frost-ed448/tests/recreation_tests.rs +++ b/frost-ed448/tests/recreation_tests.rs @@ -54,7 +54,7 @@ fn check_signature_share_recreation() { let encoded = signature_share.serialize(); - let new_signature_share = SignatureShare::deserialize(encoded).unwrap(); + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); assert!(signature_share == new_signature_share); } @@ -101,12 +101,51 @@ fn check_public_key_package_recreation() { let verifying_shares = public_key_package.verifying_shares(); let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); - let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); assert!(public_key_package == new_public_key_package); } +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + /// Check if round1::Package can be recreated. #[test] fn check_round1_package_recreation() { @@ -120,6 +159,28 @@ fn check_round1_package_recreation() { assert!(round1_package == new_round1_package); } +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + /// Check if round2::Package can be recreated. #[test] fn check_round2_package_recreation() { diff --git a/frost-ed448/tests/rerandomized_tests.rs b/frost-ed448/tests/rerandomized_tests.rs index 16e31f23a..e16d906ce 100644 --- a/frost-ed448/tests/rerandomized_tests.rs +++ b/frost-ed448/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_ed448::Ed448Shake256; -use rand::thread_rng; #[test] fn check_randomized_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let (_msg, _group_signature, _group_pubkey) = frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); diff --git a/frost-ed448/tests/serde_tests.rs b/frost-ed448/tests/serde_tests.rs index 3b5c667a4..db380eed1 100644 --- a/frost-ed448/tests/serde_tests.rs +++ b/frost-ed448/tests/serde_tests.rs @@ -434,7 +434,7 @@ fn check_key_package_serialization() { #[test] fn check_public_key_package_serialization() { - let public_key_package = samples::public_key_package(); + let public_key_package = samples::public_key_package_new(); let json = serde_json::to_string_pretty(&public_key_package).unwrap(); println!("{}", json); @@ -450,11 +450,27 @@ fn check_public_key_package_serialization() { "verifying_shares": { "2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900" }, - "verifying_key": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900" + "verifying_key": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900", + "min_signers": 2 }"#; let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); assert!(public_key_package == decoded_public_key_package); + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-ED448-SHAKE256-v1" + }, + "verifying_shares": { + "2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900" + }, + "verifying_key": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + let invalid_json = "{}"; assert!(serde_json::from_str::(invalid_json).is_err()); diff --git a/frost-ed448/tests/serialization_tests.rs b/frost-ed448/tests/serialization_tests.rs index 761e56b78..e9810a6b1 100644 --- a/frost-ed448/tests/serialization_tests.rs +++ b/frost-ed448/tests/serialization_tests.rs @@ -49,8 +49,11 @@ fn check_signing_package_postcard_serialization() { fn check_signature_share_postcard_serialization() { let signature_share = samples::signature_share(); let bytes = signature_share.serialize(); - assert_snapshot!(hex::encode(bytes)); - assert_eq!(signature_share, SignatureShare::deserialize(bytes).unwrap()); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); } #[test] fn check_secret_share_postcard_serialization() { @@ -79,6 +82,28 @@ fn check_public_key_package_postcard_serialization() { ); } +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round1_package_postcard_serialization() { let round1_package = samples::round1_package(); @@ -90,6 +115,17 @@ fn check_round1_package_postcard_serialization() { ); } +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round2_package_postcard_serialization() { let round2_package = samples::round2_package(); diff --git a/frost-ed448/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-ed448/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..fcfcff41b --- /dev/null +++ b/frost-ed448/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed448/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +005a064cfd012a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f690014fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69000102 diff --git a/frost-ed448/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-ed448/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..3094d2379 --- /dev/null +++ b/frost-ed448/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed448/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a004d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a000114fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69000203 diff --git a/frost-ed448/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-ed448/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..e4a9a0244 --- /dev/null +++ b/frost-ed448/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ed448/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000114fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69004d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a000203 diff --git a/frost-p256/CHANGELOG.md b/frost-p256/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-p256/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-p256/Cargo.toml b/frost-p256/Cargo.toml index f0dd0786b..68e63b3cf 100644 --- a/frost-p256/Cargo.toml +++ b/frost-p256/Cargo.toml @@ -1,21 +1,14 @@ [package] name = "frost-p256" -edition = "2021" -# When releasing to crates.io: -# - Update html_root_url -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = [ - "Deirdre Connolly ", - "Chelsea Komlo ", - "Conrado Gouvea " -] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] -keywords = ["cryptography", "crypto", "threshold", "signature"] +license.workspace = true +repository.workspace = true +categories.workspace = true +keywords = ["cryptography", "crypto", "p256", "threshold", "signature"] description = "A Schnorr signature scheme over the NIST P-256 curve that supports FROST." [package.metadata.docs.rs] @@ -23,36 +16,35 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -document-features = "0.2.7" -p256 = { version = "0.13.0", features = ["hash2curve"] } -frost-core = { path = "../frost-core", version = "1.0.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } -rand_core = "0.6" -sha2 = "0.10.2" +document-features.workspace = true +p256 = { version = "0.13.0", features = ["hash2curve"], default-features = false } +frost-core.workspace = true +frost-rerandomized.workspace = true +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] -criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } -insta = { version = "1.31.0", features = ["yaml"] } -hex = "0.4.3" -lazy_static = "1.4" -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +criterion.workspace = true +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +insta.workspace = true +hex.workspace = true +lazy_static.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). serde = ["frost-core/serde"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-p256/README.md b/frost-p256/README.md index 7f0a20fe4..47f391fb2 100644 --- a/frost-p256/README.md +++ b/frost-p256/README.md @@ -1,6 +1,13 @@ An implementation of Schnorr signatures on the P-256 curve for both single and threshold numbers of signers (FROST). +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +P-256 curve. For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + + + ## Example: key generation with trusted dealer and FROST signing Creating a key with a trusted dealer and splitting into shares; then signing a message @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_p256 as frost; -use rand::thread_rng; use std::collections::BTreeMap; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; let (shares, pubkey_package) = frost::keys::generate_with_dealer( @@ -45,14 +51,14 @@ let mut commitments_map = BTreeMap::new(); //////////////////////////////////////////////////////////////////////////// // In practice, each iteration of this loop will be executed by its respective participant. -for participant_index in 1..(min_signers as u16 + 1) { +for participant_index in 1..=min_signers { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _threshold_. # // ANCHOR: round1_commit let (nonces, commitments) = frost::round1::commit( - key_packages[&participant_identifier].signing_share(), + key_package.signing_share(), &mut rng, ); # // ANCHOR_END: round1_commit diff --git a/frost-p256/benches/bench.rs b/frost-p256/benches/bench.rs index 1a4d835a4..8ae524f5c 100644 --- a/frost-p256/benches/bench.rs +++ b/frost-p256/benches/bench.rs @@ -1,16 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_p256::*; fn bench_p256_batch_verify(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_batch_verify::(c, "p256", &mut rng); } fn bench_p256_sign(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_sign::(c, "p256", &mut rng); } diff --git a/frost-p256/dkg.md b/frost-p256/dkg.md index 9a3c4327d..cd64fd941 100644 --- a/frost-p256/dkg.md +++ b/frost-p256/dkg.md @@ -3,35 +3,27 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from -a `u16`. The process in which these identifiers are allocated is up to the application. - -The distributed key generation process has 3 parts, with 2 communication rounds -between them, in which each participant needs to send a "package" to every other -participant. In the first round, each participant sends the same package -(a [`round1::Package`]) to every other. In the second round, each receiver gets -their own package (a [`round2::Package`]). - -Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] -that MUST be kept secret. Between part 2 and 3, each participant needs to hold -onto a [`round2::SecretPackage`]. - -After the third part, each participant will get a [`KeyPackage`] with their -long-term secret share that must be kept secret, and a [`PublicKeyPackage`] -that is public (and will be the same between all participants). With those -they can proceed to sign messages with FROST. - +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). ## Example +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + ```rust # // ANCHOR: dkg_import -use rand::thread_rng; use std::collections::BTreeMap; use frost_p256 as frost; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; @@ -48,7 +40,10 @@ let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG protocol. @@ -70,7 +65,10 @@ for participant_index in 1..=max_signers { // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). for receiver_participant_index in 1..=max_signers { if receiver_participant_index == participant_index { continue; @@ -96,7 +94,8 @@ let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG protocol. @@ -118,7 +117,8 @@ for participant_index in 1..=max_signers { // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { diff --git a/frost-p256/src/keys/dkg.rs b/frost-p256/src/keys/dkg.rs index ae7dec6cf..dfd95384a 100644 --- a/frost-p256/src/keys/dkg.rs +++ b/frost-p256/src/keys/dkg.rs @@ -45,7 +45,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -55,13 +55,21 @@ pub fn part1( frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, @@ -70,14 +78,22 @@ pub fn part2( } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, diff --git a/frost-p256/src/keys/refresh.rs b/frost-p256/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-p256/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-p256/src/keys/repairable.rs b/frost-p256/src/keys/repairable.rs index 310a26f75..98db9d1aa 100644 --- a/frost-p256/src/keys/repairable.rs +++ b/frost-p256/src/keys/repairable.rs @@ -4,61 +4,67 @@ //! The RTS is used to help a signer (participant) repair their lost share. This is achieved //! using a subset of the other signers know here as `helpers`. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; +use crate::keys::{KeyPackage, PublicKeyPackage}; // This is imported separately to make `gencode` work. // (if it were below, the position of the import would vary between ciphersuites // after `cargo fmt`) -use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; use crate::{Error, P256Sha256}; -use super::{SecretShare, VerifiableSecretSharingCommitment}; +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; -/// Step 1 of RTS. +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Error> { - frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) } -/// Step 2 of RTS. -/// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// Part 2 of RTS. /// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { - frost::keys::repairable::repair_share_step_2::(deltas_j) +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) } -/// Step 3 of RTS +/// Part 3 of RTS. /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { - frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) } #[cfg(test)] mod tests { use lazy_static::lazy_static; - use rand::thread_rng; + use serde_json::Value; use crate::P256Sha256; @@ -70,30 +76,30 @@ mod tests { } #[test] - fn check_repair_share_step_1() { - let rng = thread_rng(); + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; - frost_core::tests::repairable::check_repair_share_step_1::(rng); + frost_core::tests::repairable::check_repair_share_part1::(rng); } #[test] - fn check_repair_share_step_2() { - frost_core::tests::repairable::check_repair_share_step_2::(&REPAIR_SHARE); + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::(&REPAIR_SHARE); } #[test] - fn check_repair_share_step_3() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_3::( + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( rng, &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_1_fails_with_invalid_min_signers() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< P256Sha256, _, >(rng); diff --git a/frost-p256/src/lib.rs b/frost-p256/src/lib.rs index 90f616d8e..42aa94f1b 100644 --- a/frost-p256/src/lib.rs +++ b/frost-p256/src/lib.rs @@ -1,11 +1,13 @@ +#![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc = document_features::document_features!()] -use std::collections::BTreeMap; +extern crate alloc; + +use alloc::collections::BTreeMap; use frost_rerandomized::RandomizedCiphersuite; use p256::{ @@ -25,7 +27,9 @@ use frost_core as frost; mod tests; // Re-exports in our public API -pub use frost_core::{serde, Ciphersuite, Field, FieldError, Group, GroupError}; +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; pub use rand_core; /// An error. @@ -112,21 +116,15 @@ impl Group for P256Group { ProjectivePoint::GENERATOR } - fn serialize(element: &Self::Element) -> Self::Serialization { + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } let mut fixed_serialized = [0; 33]; let serialized_point = element.to_encoded_point(true); let serialized = serialized_point.as_bytes(); - // Sanity check; either it takes all bytes or a single byte (identity). - assert!(serialized.len() == fixed_serialized.len() || serialized.len() == 1); - // Copy to the left of the buffer (i.e. pad the identity with zeroes). - // Note that identity elements shouldn't be serialized in FROST, but we - // do this padding so that this function doesn't have to return an error. - // If this encodes the identity, it will fail when deserializing. - { - let (left, _right) = fixed_serialized.split_at_mut(serialized.len()); - left.copy_from_slice(serialized); - } - fixed_serialized + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) } fn deserialize(buf: &Self::Serialization) -> Result { @@ -155,20 +153,20 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { h.update(i); } let mut output = [0u8; 32]; - output.copy_from_slice(h.finalize().as_slice()); + output.copy_from_slice(h.finalize().as_ref()); output } -fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { let mut u = [P256ScalarField::zero()]; - hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + hash_to_field::, Scalar>(&[msg], domain, &mut u) .expect("should never return error according to error cases described in ExpandMsgXmd"); u[0] } /// Context string from the ciphersuite in the [spec] /// -/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-1 +/// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-1 const CONTEXT_STRING: &str = "FROST-P256-SHA256-v1"; /// An implementation of the FROST(P-256, SHA-256) ciphersuite. @@ -186,60 +184,54 @@ impl Ciphersuite for P256Sha256 { /// H1 for FROST(P-256, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.1 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) } /// H2 for FROST(P-256, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.2 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "chal").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"chal"], m) } /// H3 for FROST(P-256, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.3 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) } /// H4 for FROST(P-256, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.8 fn H4(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) } /// H5 for FROST(P-256, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.5 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.10 fn H5(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) } /// HDKG for FROST(P-256, SHA-256) fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "dkg").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) } /// HID for FROST(P-256, SHA-256) fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "id").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) } } impl RandomizedCiphersuite for P256Sha256 { fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "randomizer").as_bytes(), + &[CONTEXT_STRING.as_bytes(), b"randomizer"], m, )) } @@ -252,8 +244,6 @@ type P = P256Sha256; pub type Identifier = frost::Identifier

; /// FROST(P-256, SHA-256) keys, key generation, key shares. pub mod keys { - use std::collections::BTreeMap; - use super::*; /// The identifier list to use when generating key shares. @@ -345,6 +335,7 @@ pub mod keys { pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment

; pub mod dkg; + pub mod refresh; pub mod repairable; } @@ -437,6 +428,25 @@ pub fn aggregate( frost::aggregate(signing_package, signature_shares, pubkeys) } +/// The type of cheater detection to use. +pub type CheaterDetection = frost::CheaterDetection; + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result { + frost::aggregate_custom( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + ) +} + /// A signing key for a Schnorr signature on FROST(P-256, SHA-256). pub type SigningKey = frost_core::SigningKey

; diff --git a/frost-p256/src/rerandomized.rs b/frost-p256/src/rerandomized.rs new file mode 100644 index 000000000..e5e8b4411 --- /dev/null +++ b/frost-p256/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-p256/src/tests/batch.rs b/frost-p256/src/tests/batch.rs index 1d4cff1a7..3a46bfdd3 100644 --- a/frost-p256/src/tests/batch.rs +++ b/frost-p256/src/tests/batch.rs @@ -1,24 +1,22 @@ -use rand::thread_rng; - use crate::*; #[test] fn check_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::batch_verify::(rng); } #[test] fn check_bad_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::bad_batch_verify::(rng); } #[test] fn empty_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::empty_batch_verify::(rng); } diff --git a/frost-p256/src/tests/coefficient_commitment.rs b/frost-p256/src/tests/coefficient_commitment.rs index 8083aea08..e52f8398f 100644 --- a/frost-p256/src/tests/coefficient_commitment.rs +++ b/frost-p256/src/tests/coefficient_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,7 +12,7 @@ lazy_static! { #[test] fn check_serialization_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< P256Sha256, _, @@ -22,7 +21,7 @@ fn check_serialization_of_coefficient_commitment() { #[test] fn check_create_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::( rng, ); @@ -36,7 +35,7 @@ fn check_create_coefficient_commitment_error() { #[test] fn check_get_value_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< P256Sha256, diff --git a/frost-p256/src/tests/deserialize.rs b/frost-p256/src/tests/deserialize.rs index 38aa9e65b..ab7d8bf2f 100644 --- a/frost-p256/src/tests/deserialize.rs +++ b/frost-p256/src/tests/deserialize.rs @@ -4,7 +4,8 @@ use crate::*; fn check_deserialize_non_canonical() { let mut encoded_generator = ::Group::serialize( &::Group::generator(), - ); + ) + .unwrap(); let r = ::Group::deserialize(&encoded_generator); assert!(r.is_ok()); diff --git a/frost-p256/src/tests/vss_commitment.rs b/frost-p256/src/tests/vss_commitment.rs index a30d3f62f..44c08c5be 100644 --- a/frost-p256/src/tests/vss_commitment.rs +++ b/frost-p256/src/tests/vss_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,26 +12,46 @@ lazy_static! { #[test] fn check_serialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); } +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::(rng); +} + #[test] fn check_deserialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment::(rng); } +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::(rng); +} + #[test] fn check_deserialize_vss_commitment_error() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( rng, &ELEMENTS, ); } +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::( + rng, &ELEMENTS, + ); +} + #[test] fn check_compute_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_compute_public_key_package::(rng); } diff --git a/frost-p256/tests/common_traits_tests.rs b/frost-p256/tests/common_traits_tests.rs index 16f52d5b7..8dc2d739d 100644 --- a/frost-p256/tests/common_traits_tests.rs +++ b/frost-p256/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_p256::SigningKey; use helpers::samples; -use rand::thread_rng; #[allow(clippy::unnecessary_literal_unwrap)] fn check_common_traits_for_type(v: T) { @@ -20,7 +19,7 @@ fn check_common_traits_for_type(v: #[test] fn check_signing_key_common_traits() { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; let signing_key = SigningKey::new(&mut rng); check_common_traits_for_type(signing_key); } diff --git a/frost-p256/tests/helpers/samples.json b/frost-p256/tests/helpers/samples.json index 928e355c7..3fe4c6987 100644 --- a/frost-p256/tests/helpers/samples.json +++ b/frost-p256/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1", "element1": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", "element2": "037cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc47669978", "scalar1": "aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1" -} \ No newline at end of file +} diff --git a/frost-p256/tests/helpers/samples.rs b/frost-p256/tests/helpers/samples.rs index 0158a8bc4..e6a2fd260 100644 --- a/frost-p256/tests/helpers/samples.rs +++ b/frost-p256/tests/helpers/samples.rs @@ -36,18 +36,20 @@ fn scalar1() -> Scalar { pub fn signing_nonces() -> SigningNonces { let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); - let hiding_nonce = Nonce::deserialize(serialized_scalar1).unwrap(); - let binding_nonce = Nonce::deserialize(serialized_scalar2).unwrap(); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); SigningNonces::from_nonces(hiding_nonce, binding_nonce) } /// Generate a sample SigningCommitments. pub fn signing_commitments() -> SigningCommitments { - let serialized_element1 = ::Group::serialize(&element1()); - let serialized_element2 = ::Group::serialize(&element2()); - let hiding_nonce_commitment = NonceCommitment::deserialize(serialized_element1).unwrap(); - let binding_nonce_commitment = NonceCommitment::deserialize(serialized_element2).unwrap(); + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) } @@ -65,15 +67,15 @@ pub fn signing_package() -> SigningPackage { pub fn signature_share() -> SignatureShare { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - SignatureShare::deserialize(serialized_scalar).unwrap() + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() } /// Generate a sample SecretShare. pub fn secret_share() -> SecretShare { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); @@ -84,11 +86,11 @@ pub fn secret_share() -> SecretShare { pub fn key_package() -> KeyPackage { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) } @@ -96,38 +98,82 @@ pub fn key_package() -> KeyPackage { /// Generate a sample PublicKeyPackage. pub fn public_key_package() -> PublicKeyPackage { let identifier = 42u16.try_into().unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) } /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>() - .try_into() - .unwrap(); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + /// Generate a sample round2::Package. pub fn round2_package() -> round2::Package { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); round2::Package::new(signing_share) } diff --git a/frost-p256/tests/integration_tests.rs b/frost-p256/tests/integration_tests.rs index f25733538..d1d27f440 100644 --- a/frost-p256/tests/integration_tests.rs +++ b/frost-p256/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_p256::*; use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; #[test] @@ -10,14 +9,14 @@ fn check_zero_key_fails() { #[test] fn check_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); } #[test] fn check_dkg_part1_fails_with_invalid_signers_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -31,7 +30,7 @@ fn check_dkg_part1_fails_with_invalid_signers_min_signers() { #[test] fn check_dkg_part1_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -45,7 +44,7 @@ fn check_dkg_part1_fails_with_min_signers_greater_than_max() { #[test] fn check_dkg_part1_fails_with_invalid_signers_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -59,21 +58,80 @@ fn check_dkg_part1_fails_with_invalid_signers_max_signers() { #[test] fn check_rts() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( + rng, + ); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + P256Sha256, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + P256Sha256, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::( + rng, + ); +} + #[test] fn check_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); } #[test] fn check_sign_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -87,7 +145,7 @@ fn check_sign_with_dealer_fails_with_invalid_min_signers() { #[test] fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -101,7 +159,7 @@ fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { #[test] fn check_sign_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -117,13 +175,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_p256_sha256() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_share_generation::(rng); } #[test] fn check_share_generation_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 0; let max_signers = 3; @@ -137,7 +195,7 @@ fn check_share_generation_fails_with_invalid_min_signers() { #[test] fn check_share_generation_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -151,7 +209,7 @@ fn check_share_generation_fails_with_min_signers_greater_than_max() { #[test] fn check_share_generation_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 0; @@ -213,7 +271,7 @@ fn check_identifier_generation() -> Result<(), Error> { #[test] fn check_sign_with_dealer_and_identifiers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::( rng, @@ -222,7 +280,7 @@ fn check_sign_with_dealer_and_identifiers() { #[test] fn check_sign_with_missing_identifier() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::( rng, ); @@ -230,8 +288,18 @@ fn check_sign_with_missing_identifier() { #[test] fn check_sign_with_incorrect_commitments() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::( rng, ); } + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng).await; + }) + .await + .unwrap(); +} diff --git a/frost-p256/tests/recreation_tests.rs b/frost-p256/tests/recreation_tests.rs index 41ea2802f..99c39a2d1 100644 --- a/frost-p256/tests/recreation_tests.rs +++ b/frost-p256/tests/recreation_tests.rs @@ -54,7 +54,7 @@ fn check_signature_share_recreation() { let encoded = signature_share.serialize(); - let new_signature_share = SignatureShare::deserialize(encoded).unwrap(); + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); assert!(signature_share == new_signature_share); } @@ -101,12 +101,51 @@ fn check_public_key_package_recreation() { let verifying_shares = public_key_package.verifying_shares(); let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); - let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); assert!(public_key_package == new_public_key_package); } +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + /// Check if round1::Package can be recreated. #[test] fn check_round1_package_recreation() { @@ -120,6 +159,28 @@ fn check_round1_package_recreation() { assert!(round1_package == new_round1_package); } +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + /// Check if round2::Package can be recreated. #[test] fn check_round2_package_recreation() { diff --git a/frost-p256/tests/rerandomized_tests.rs b/frost-p256/tests/rerandomized_tests.rs index d16a0c304..6dc482ce6 100644 --- a/frost-p256/tests/rerandomized_tests.rs +++ b/frost-p256/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_p256::P256Sha256; -use rand::thread_rng; #[test] fn check_randomized_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let (_msg, _group_signature, _group_pubkey) = frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); diff --git a/frost-p256/tests/serde_tests.rs b/frost-p256/tests/serde_tests.rs index c14758146..7091bd7b3 100644 --- a/frost-p256/tests/serde_tests.rs +++ b/frost-p256/tests/serde_tests.rs @@ -434,7 +434,7 @@ fn check_key_package_serialization() { #[test] fn check_public_key_package_serialization() { - let public_key_package = samples::public_key_package(); + let public_key_package = samples::public_key_package_new(); let json = serde_json::to_string_pretty(&public_key_package).unwrap(); println!("{}", json); @@ -450,11 +450,27 @@ fn check_public_key_package_serialization() { "verifying_shares": { "000000000000000000000000000000000000000000000000000000000000002a": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296" }, - "verifying_key": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296" + "verifying_key": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + "min_signers": 2 }"#; let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); assert!(public_key_package == decoded_public_key_package); + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-P256-SHA256-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296" + }, + "verifying_key": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + let invalid_json = "{}"; assert!(serde_json::from_str::(invalid_json).is_err()); diff --git a/frost-p256/tests/serialization_tests.rs b/frost-p256/tests/serialization_tests.rs index a920b6c2e..dbd32cec3 100644 --- a/frost-p256/tests/serialization_tests.rs +++ b/frost-p256/tests/serialization_tests.rs @@ -49,8 +49,11 @@ fn check_signing_package_postcard_serialization() { fn check_signature_share_postcard_serialization() { let signature_share = samples::signature_share(); let bytes = signature_share.serialize(); - assert_snapshot!(hex::encode(bytes)); - assert_eq!(signature_share, SignatureShare::deserialize(bytes).unwrap()); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); } #[test] fn check_secret_share_postcard_serialization() { @@ -79,6 +82,28 @@ fn check_public_key_package_postcard_serialization() { ); } +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round1_package_postcard_serialization() { let round1_package = samples::round1_package(); @@ -90,6 +115,17 @@ fn check_round1_package_postcard_serialization() { ); } +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round2_package_postcard_serialization() { let round2_package = samples::round2_package(); diff --git a/frost-p256/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-p256/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..158419f6e --- /dev/null +++ b/frost-p256/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-p256/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00a132f0c901000000000000000000000000000000000000000000000000000000000000002a036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2960102 diff --git a/frost-p256/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-p256/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..5e9442903 --- /dev/null +++ b/frost-p256/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-p256/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a02aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e101036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2960203 diff --git a/frost-p256/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-p256/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..f4c72b402 --- /dev/null +++ b/frost-p256/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-p256/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a01036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e10203 diff --git a/frost-rerandomized/CHANGELOG.md b/frost-rerandomized/CHANGELOG.md new file mode 100644 index 000000000..5a1865710 --- /dev/null +++ b/frost-rerandomized/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +Entries are listed in reverse chronological order. + + +## Unreleased + + +## 3.0.0-rc.0 + +### Breaking Changes + +* The `cheater-detection` feature was removed. If you relied on it (either by + using the default features, or by explicitly enabling it), then you don't have + to do anything (other than not enabling it explicitly if you were doing so); + the default behaviour is now as if `cheater-detection` was enabled. If you + explicitly *did not enable* it, you can avoid cheater detection by calling + `aggregate_custom()` with `CheaterDetection::Disabled`. + +### Added + +There is a new revamped API which was motivated by integration with Zcash +but should have broader application. + +- Added `RandomizedParams::new_from_commitments()` which will generate the + randomizer based on the signing commitments and on some fresh random data. + This is better since all parties will contribute to the randomness of the + randomizer. The random data ("seed") will be returned along with the + `RandomizedParams`. +- Added `RandomizedParams::regenerate_from_seed_and_commitments()` which will + redo the procedure above with a given seed. +- Added `sign_with_randomizer_seed()` which is a helper function that will + rebuild the `RandomizedParams` with a given seed and proceed with the + signing. +- Added `Randomizer::{new_from_commitments(), regenerate_from_seed_and_commitments()}` + which are used by the above and will probably not need to be called directly. diff --git a/frost-rerandomized/Cargo.toml b/frost-rerandomized/Cargo.toml index 71c37335f..2579900e9 100644 --- a/frost-rerandomized/Cargo.toml +++ b/frost-rerandomized/Cargo.toml @@ -1,17 +1,13 @@ [package] name = "frost-rerandomized" -edition = "2021" -# When releasing to crates.io: -# - Update html_root_url -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = ["Deirdre Connolly ", "Chelsea Komlo ", - "Conrado Gouvea "] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] +license.workspace = true +repository.workspace = true +categories.workspace = true keywords = ["cryptography", "threshold", "signature", "schnorr", "randomized"] description = "Types and traits to support implementing a re-randomized variant of Flexible Round-Optimized Schnorr Threshold signature schemes (FROST)." @@ -20,23 +16,22 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -derive-getters = "0.3.0" -document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0", features = ["internals"] } -rand_core = "0.6" +derive-getters = "0.5.0" +document-features.workspace = true +frost-core = { workspace = true, features = ["internals"] } +hex.workspace = true +rand_core.workspace = true [dev-dependencies] [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). serde = ["frost-core/serde"] # Exposes ciphersuite-generic tests for other crates to use -test-impl = ["frost-core/test-impl"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +test-impl = ["frost-core/test-impl", "serialization"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization"] diff --git a/frost-rerandomized/README.md b/frost-rerandomized/README.md index 733b6e4be..543f13b1b 100644 --- a/frost-rerandomized/README.md +++ b/frost-rerandomized/README.md @@ -1,22 +1,19 @@ # FROST (Flexible Round-Optimised Schnorr Threshold signatures) Rerandomized -Base traits and types in Rust that implement ['Two-Round Threshold Schnorr Signatures with -FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) generically for -`frost-core::Ciphersuite` implementations, with support for Zcash-compatible -RedDSA re-randomized signatures. - -## Status ⚠ - -The FROST specification is not yet finalized, and this codebase has not yet been audited or -released. The APIs and types in `frost-rerandomized` are subject to change. +A ciphersuite-generic implementation of [Re-Randomized +FROST](https://eprint.iacr.org/2024/436), which allows creating signatures using +FROST under re-randomized keys. ## Usage `frost-rerandomized` is similar to `frost-core`, but provides different `sign()` and `aggregate()` functions adding support for re-randomized signatures. -End-users should not use `frost-rerandomized` if they want to sign and verify signatures, they -should use the crate specific to their ciphersuite/curve parameters that uses `frost-rerandomized` as a -dependency, such as [`reddsa`](https://github.com/ZcashFoundation/reddsa/). + +Currently, the main ciphersuite crates do not re-expose the rerandomization +functions; if you want to use this functionality, you will need to use this +crate parametrized with the chosen ciphersuite. The exceptions are the Zcash +ciphersuites in [`reddsa`](https://github.com/ZcashFoundation/reddsa/) which +do expose the randomized functionality. ## Example diff --git a/frost-rerandomized/src/lib.rs b/frost-rerandomized/src/lib.rs index f4ff81b91..0b5e05715 100644 --- a/frost-rerandomized/src/lib.rs +++ b/frost-rerandomized/src/lib.rs @@ -3,32 +3,40 @@ //! To sign with re-randomized FROST: //! //! - Do Round 1 the same way as regular FROST; -//! - The Coordinator should call [`RandomizedParams::new()`] and send -//! the [`RandomizedParams::randomizer`] to all participants, using a -//! confidential channel, along with the regular [`frost::SigningPackage`]; -//! - Each participant should call [`sign`] and send the resulting +//! - The Coordinator should call [`RandomizedParams::new_from_commitments()`] +//! and send the generate randomizer seed (the second returned value) to all +//! participants, using a confidential channel, along with the regular +//! [`frost::SigningPackage`]; +//! - Each participant should regenerate the RandomizerParams by calling +//! [`RandomizedParams::regenerate_from_seed_and_commitments()`], which they +//! should pass to [`sign_with_randomizer_seed()`] and send the resulting //! [`frost::round2::SignatureShare`] back to the Coordinator; //! - The Coordinator should then call [`aggregate`]. +#![no_std] #![allow(non_snake_case)] +extern crate alloc; + #[cfg(any(test, feature = "test-impl"))] pub mod tests; -use std::collections::BTreeMap; +use alloc::{collections::BTreeMap, string::ToString, vec::Vec}; use derive_getters::Getters; pub use frost_core; +#[cfg(feature = "serialization")] +use frost_core::SigningPackage; use frost_core::{ self as frost, keys::{KeyPackage, PublicKeyPackage, SigningShare, VerifyingShare}, - Ciphersuite, Error, Field, Group, Scalar, SigningPackage, VerifyingKey, + round1::{encode_group_commitments, SigningCommitments}, + serialization::SerializableScalar, + CheaterDetection, Ciphersuite, Error, Field, Group, Identifier, Scalar, VerifyingKey, }; #[cfg(feature = "serde")] use frost_core::serde; -#[cfg(feature = "serde")] -use frost_core::serialization::ScalarSerialization; // When pulled into `reddsa`, that has its own sibling `rand_core` import. // For the time being, we do not re-export this `rand_core`. @@ -69,7 +77,7 @@ impl Randomize for KeyPackage { let signing_share = self.signing_share(); let randomized_signing_share = - SigningShare::new(signing_share.to_scalar() + randomized_params.randomizer.0); + SigningShare::new(signing_share.to_scalar() + randomized_params.randomizer.to_scalar()); let randomized_key_package = KeyPackage::new( *self.identifier(), @@ -106,9 +114,10 @@ impl Randomize for PublicKeyPackage { }) .collect(); - Ok(PublicKeyPackage::new( + Ok(PublicKeyPackage::new_internal( randomized_verifying_shares, randomized_params.randomized_verifying_key, + self.min_signers(), )) } } @@ -117,6 +126,9 @@ impl Randomize for PublicKeyPackage { /// be sent from the Coordinator using a confidential channel. /// /// See [`frost::round2::sign`] for documentation on the other parameters. +#[deprecated( + note = "switch to sign_with_randomizer_seed(), passing a seed generated with RandomizedParams::new_from_commitments()" +)] pub fn sign( signing_package: &frost::SigningPackage, signer_nonces: &frost::round1::SigningNonces, @@ -129,9 +141,27 @@ pub fn sign( frost::round2::sign(signing_package, signer_nonces, &randomized_key_package) } -/// Re-randomized FROST signature share aggregation with the given [`RandomizedParams`], -/// which can be computed from the previously generated randomizer using -/// [`RandomizedParams::from_randomizer`]. +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`frost::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &frost::SigningPackage, + signer_nonces: &frost::round1::SigningNonces, + key_package: &frost::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result, Error> { + let randomized_params = RandomizedParams::regenerate_from_seed_and_commitments( + key_package.verifying_key(), + randomizer_seed, + signing_package.signing_commitments(), + )?; + let randomized_key_package = key_package.randomize(&randomized_params)?; + frost::round2::sign(signing_package, signer_nonces, &randomized_key_package) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. /// /// See [`frost::aggregate`] for documentation on the other parameters. pub fn aggregate( @@ -151,24 +181,59 @@ where ) } +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &frost::SigningPackage, + signature_shares: &BTreeMap, frost::round2::SignatureShare>, + pubkeys: &frost::keys::PublicKeyPackage, + cheater_detection: CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result, Error> +where + C: Ciphersuite, +{ + let randomized_public_key_package = pubkeys.randomize(randomized_params)?; + frost::aggregate_custom( + signing_package, + signature_shares, + &randomized_public_key_package, + cheater_detection, + ) +} + /// A randomizer. A random scalar which is used to randomize the key. #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] -#[cfg_attr(feature = "serde", serde(try_from = "ScalarSerialization"))] -#[cfg_attr(feature = "serde", serde(into = "ScalarSerialization"))] +#[cfg_attr(feature = "serde", serde(transparent))] #[cfg_attr(feature = "serde", serde(crate = "self::serde"))] -pub struct Randomizer(Scalar); +pub struct Randomizer(SerializableScalar); + +impl Randomizer +where + C: Ciphersuite, +{ + pub(crate) fn to_scalar(self) -> Scalar { + self.0 .0 + } +} impl Randomizer where C: RandomizedCiphersuite, { - /// Create a new random Randomizer. + /// Create a new random Randomizer using a SigningPackage for randomness. /// /// The [`SigningPackage`] must be the signing package being used in the /// current FROST signing run. It is hashed into the randomizer calculation, /// which binds it to that specific package. + #[cfg(feature = "serialization")] + #[deprecated( + note = "switch to new_from_commitments(), passing the commitments from SigningPackage" + )] pub fn new( mut rng: R, signing_package: &SigningPackage, @@ -179,6 +244,7 @@ where /// Create a final Randomizer from a random Randomizer and a SigningPackage. /// Function refactored out for testing, should always be private. + #[cfg(feature = "serialization")] fn from_randomizer_and_signing_package( rng_randomizer: <<::Group as Group>::Field as Field>::Scalar, signing_package: &SigningPackage, @@ -194,7 +260,66 @@ where .concat(), ) .ok_or(Error::SerializationError)?; - Ok(Self(randomizer)) + Ok(Self(SerializableScalar(randomizer))) + } + + /// Create a new random Randomizer using SigningCommitments for randomness. + /// + /// The [`SigningCommitments`] map must be the one being used in the current + /// FROST signing run (built by the Coordinator after receiving from + /// Participants). It is hashed into the randomizer calculation, which binds + /// it to that specific commitments. + /// + /// Returns the Randomizer and the generate randomizer seed. Both can be + /// used to regenerate the Randomizer with + /// [`Self::regenerate_from_seed_and_commitments()`]. + pub fn new_from_commitments( + mut rng: R, + signing_commitments: &BTreeMap, SigningCommitments>, + ) -> Result<(Self, Vec), Error> { + // Generate a dummy scalar to get its encoded size + let zero = <::Field as Field>::zero(); + let ns = <::Field as Field>::serialize(&zero) + .as_ref() + .len(); + let mut randomizer_seed = alloc::vec![0; ns]; + rng.fill_bytes(&mut randomizer_seed); + Ok(( + Self::regenerate_from_seed_and_commitments(&randomizer_seed, signing_commitments)?, + randomizer_seed, + )) + } + + /// Regenerates a Randomizer generated with + /// [`Self::new_from_commitments()`]. This can be used by Participants after + /// receiving the randomizer seed and commitments in Round 2. This is better + /// than the Coordinator simply generating a Randomizer and sending it to + /// Participants, because in this approach the participants don't need to + /// fully trust the Coordinator's random number generator (i.e. even if the + /// randomizer seed was not randomly generated the randomizer will still + /// be). + /// + /// This should be used exclusively with the output of + /// [`Self::new_from_commitments()`]; it is strongly suggested to not + /// attempt generating the randomizer seed yourself (even if the point of + /// this approach is to hedge against issues in the randomizer seed + /// generation). + pub fn regenerate_from_seed_and_commitments( + randomizer_seed: &[u8], + signing_commitments: &BTreeMap, SigningCommitments>, + ) -> Result, Error> + where + C: RandomizedCiphersuite, + { + let randomizer = C::hash_randomizer( + &[ + randomizer_seed, + &encode_group_commitments(signing_commitments)?, + ] + .concat(), + ) + .ok_or(Error::SerializationError)?; + Ok(Self(SerializableScalar(randomizer))) } } @@ -209,43 +334,29 @@ where /// reasons with specifications on how the randomizer must be generated. Use /// [`Randomizer::new()`] instead. pub fn from_scalar(scalar: Scalar) -> Self { - Self(scalar) + Self(SerializableScalar(scalar)) } /// Serialize the identifier using the ciphersuite encoding. - pub fn serialize(&self) -> <::Field as Field>::Serialization { - <::Field>::serialize(&self.0) + pub fn serialize(&self) -> Vec { + self.0.serialize() } /// Deserialize an Identifier from a serialized buffer. /// Returns an error if it attempts to deserialize zero. - pub fn deserialize( - buf: &<::Field as Field>::Serialization, - ) -> Result> { - let scalar = <::Field>::deserialize(buf)?; - Ok(Self(scalar)) + pub fn deserialize(buf: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(buf)?)) } } -#[cfg(feature = "serde")] -impl TryFrom> for Randomizer -where - C: Ciphersuite, -{ - type Error = Error; - - fn try_from(value: ScalarSerialization) -> Result { - Self::deserialize(&value.0) - } -} - -#[cfg(feature = "serde")] -impl From> for ScalarSerialization +impl core::fmt::Debug for Randomizer where C: Ciphersuite, { - fn from(value: Randomizer) -> Self { - Self(value.serialize()) + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Randomizer") + .field(&hex::encode(self.0.serialize())) + .finish() } } @@ -265,17 +376,76 @@ where C: RandomizedCiphersuite, { /// Create a new [`RandomizedParams`] for the given [`VerifyingKey`] and - /// the given `participants`. + /// the given [`SigningPackage`]. + #[cfg(feature = "serialization")] + #[deprecated( + note = "switch to new_from_commitments(), passing the commitments from SigningPackage" + )] pub fn new( group_verifying_key: &VerifyingKey, signing_package: &SigningPackage, rng: R, ) -> Result> { + #[allow(deprecated)] Ok(Self::from_randomizer( group_verifying_key, Randomizer::new(rng, signing_package)?, )) } + + /// Create a new [`RandomizedParams`] for the given [`VerifyingKey`] and the + /// given signing commitments. + /// + /// The [`SigningCommitments`] map must be the one being used in the current + /// FROST signing run (built by the Coordinator after receiving from + /// Participants). It is hashed into the randomizer calculation, which binds + /// it to that specific commitments. + /// + /// Returns the generated [`RandomizedParams`] and a randomizer seed. Both + /// can be used to regenerate the [`RandomizedParams`] with + /// [`Self::regenerate_from_seed_and_commitments()`]. + pub fn new_from_commitments( + group_verifying_key: &VerifyingKey, + signing_commitments: &BTreeMap, SigningCommitments>, + rng: R, + ) -> Result<(Self, Vec), Error> { + let (randomizer, randomizer_seed) = + Randomizer::new_from_commitments(rng, signing_commitments)?; + Ok(( + Self::from_randomizer(group_verifying_key, randomizer), + randomizer_seed, + )) + } + + /// Regenerate a [`RandomizedParams`] with the given [`VerifyingKey`] from + /// the given given signing commitments. + /// + /// Returns the generated [`RandomizedParams`] and a randomizer seed, which + /// can be used to regenerate the [`RandomizedParams`]. + /// + /// Regenerates a [`RandomizedParams`] generated with + /// [`Self::new_from_commitments()`]. This can be used by Participants after + /// receiving the randomizer seed and commitments in Round 2. This is better + /// than the Coordinator simply generating a [`Randomizer`] and sending it + /// to Participants, because in this approach the participants don't need to + /// fully trust the Coordinator's random number generator (i.e. even if the + /// randomizer seed was not randomly generated the randomizer will still + /// be). + /// + /// This should be used exclusively with the output of + /// [`Self::new_from_commitments()`]; it is strongly suggested to not + /// attempt generating the randomizer seed yourself (even if the point of + /// this approach is to hedge against issues in the randomizer seed + /// generation). + pub fn regenerate_from_seed_and_commitments( + group_verifying_key: &VerifyingKey, + randomizer_seed: &[u8], + signing_commitments: &BTreeMap, SigningCommitments>, + ) -> Result> { + let randomizer = + Randomizer::regenerate_from_seed_and_commitments(randomizer_seed, signing_commitments)?; + Ok(Self::from_randomizer(group_verifying_key, randomizer)) + } } impl RandomizedParams @@ -291,7 +461,7 @@ where group_verifying_key: &VerifyingKey, randomizer: Randomizer, ) -> Self { - let randomizer_element = ::generator() * randomizer.0; + let randomizer_element = ::generator() * randomizer.to_scalar(); let verifying_key_element = group_verifying_key.to_element(); let randomized_verifying_key_element = verifying_key_element + randomizer_element; let randomized_verifying_key = VerifyingKey::::new(randomized_verifying_key_element); @@ -303,3 +473,21 @@ where } } } + +impl core::fmt::Debug for RandomizedParams +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("RandomizedParams") + .field("randomizer", &self.randomizer) + .field( + "randomizer_element", + &::serialize(&self.randomizer_element) + .map(hex::encode) + .unwrap_or("".to_string()), + ) + .field("randomized_verifying_key", &self.randomized_verifying_key) + .finish() + } +} diff --git a/frost-rerandomized/src/tests.rs b/frost-rerandomized/src/tests.rs index 15fde19bb..0b5264988 100644 --- a/frost-rerandomized/src/tests.rs +++ b/frost-rerandomized/src/tests.rs @@ -1,9 +1,14 @@ //! Ciphersuite-generic test functions for re-randomized FROST. +#![cfg(feature = "serialization")] -use std::collections::BTreeMap; +use alloc::borrow::ToOwned; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; use crate::{frost_core as frost, RandomizedCiphersuite, RandomizedParams, Randomizer}; -use frost_core::{Field, Group, Signature, SigningPackage, VerifyingKey}; +use frost_core::{ + round1::SigningCommitments, Field, Group, Identifier, Signature, SigningPackage, VerifyingKey, +}; use rand_core::{CryptoRng, RngCore}; /// Test re-randomized FROST signing with trusted dealer with a Ciphersuite. @@ -67,9 +72,12 @@ pub fn check_randomized_sign_with_dealer( check_from_randomizer(&mut rng, signing_package, pubkeys); check_from_randomizer_and_signing_package(&mut rng, signing_package); + + check_from_seed_and_signing_commitments(&mut rng, signing_package.signing_commitments()); } fn check_from_randomizer( @@ -135,6 +150,7 @@ fn check_from_randomizer( signing_package: &SigningPackage, pubkeys: &frost::keys::PublicKeyPackage, ) { + #[allow(deprecated)] let randomizer = Randomizer::new(rng, signing_package).unwrap(); let randomizer_params = RandomizedParams::from_randomizer(pubkeys.verifying_key(), randomizer); @@ -173,3 +189,39 @@ fn check_from_randomizer_and_signing_package( + mut rng: &mut R, + signing_commitments: &BTreeMap, SigningCommitments>, +) { + // Make sure regeneration returns the same Randomizer. + let (randomizer1, randomizer_seed1) = + Randomizer::new_from_commitments(&mut rng, signing_commitments).unwrap(); + let randomizer2 = + Randomizer::regenerate_from_seed_and_commitments(&randomizer_seed1, signing_commitments) + .unwrap(); + assert!(randomizer1 == randomizer2); + + let (randomizer2, randomizer_seed2) = + Randomizer::new_from_commitments(&mut rng, signing_commitments).unwrap(); + + // Make sure that different rng_randomizers lead to different randomizers + assert!(randomizer1 != randomizer2); + assert!(randomizer_seed1 != randomizer_seed2); + + // Modify the commitments map, by overwriting the first entry with the value + // of the last entry. + let mut modified_signing_commitments = signing_commitments.clone(); + modified_signing_commitments + .first_entry() + .unwrap() + .insert(*signing_commitments.last_key_value().unwrap().1); + let randomizer2 = Randomizer::regenerate_from_seed_and_commitments( + &randomizer_seed1, + &modified_signing_commitments, + ) + .unwrap(); + + // Make sure that different packages lead to different randomizers + assert!(randomizer1 != randomizer2); +} diff --git a/frost-ristretto255/CHANGELOG.md b/frost-ristretto255/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-ristretto255/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-ristretto255/Cargo.toml b/frost-ristretto255/Cargo.toml index a886f8e8d..3233dc451 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -1,16 +1,13 @@ [package] name = "frost-ristretto255" -edition = "2021" -# When releasing to crates.io: -# - Update html_root_url -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = ["Deirdre Connolly ", "Chelsea Komlo ", "Conrado Gouvea "] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] +license.workspace = true +repository.workspace = true +categories.workspace = true keywords = ["cryptography", "crypto", "ristretto", "threshold", "signature"] description = "A Schnorr signature scheme over the prime-order Ristretto group that supports FROST." @@ -19,37 +16,36 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -curve25519-dalek = { version = "=4.1.2", features = ["serde", "rand_core"] } -document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } -rand_core = "0.6" -sha2 = "0.10.2" +curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } +document-features.workspace = true +frost-core.workspace = true +frost-rerandomized.workspace = true +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } -frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } -insta = { version = "1.31.0", features = ["yaml"] } -hex = "0.4.3" -lazy_static = "1.4" +criterion = { workspace = true, features = ["html_reports"] } +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +insta.workspace = true +hex.workspace = true +lazy_static.workspace = true postcard = { version = "1.0.0", features = ["use-std"] } -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). -serde = ["frost-core/serde"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +serde = ["frost-core/serde", "curve25519-dalek/serde"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ristretto255/README.md b/frost-ristretto255/README.md index 436b953d9..0f061bd95 100644 --- a/frost-ristretto255/README.md +++ b/frost-ristretto255/README.md @@ -1,6 +1,13 @@ An implementation of Schnorr signatures on the Ristretto group for both single and threshold numbers of signers (FROST). +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +Ristretto group. For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + + + ## Example: key generation with trusted dealer and FROST signing Creating a key with a trusted dealer and splitting into shares; then signing a message @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_ristretto255 as frost; -use rand::thread_rng; use std::collections::BTreeMap; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; let (shares, pubkey_package) = frost::keys::generate_with_dealer( @@ -45,14 +51,14 @@ let mut commitments_map = BTreeMap::new(); //////////////////////////////////////////////////////////////////////////// // In practice, each iteration of this loop will be executed by its respective participant. -for participant_index in 1..(min_signers as u16 + 1) { +for participant_index in 1..=min_signers { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _threshold_. # // ANCHOR: round1_commit let (nonces, commitments) = frost::round1::commit( - key_packages[&participant_identifier].signing_share(), + key_package.signing_share(), &mut rng, ); # // ANCHOR_END: round1_commit diff --git a/frost-ristretto255/benches/bench.rs b/frost-ristretto255/benches/bench.rs index b53e560ce..b7e9af3bd 100644 --- a/frost-ristretto255/benches/bench.rs +++ b/frost-ristretto255/benches/bench.rs @@ -1,16 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_ristretto255::*; fn bench_ristretto255_batch_verify(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_batch_verify::(c, "ristretto255", &mut rng); } fn bench_ristretto255_sign(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_sign::(c, "ristretto255", &mut rng); } diff --git a/frost-ristretto255/dkg.md b/frost-ristretto255/dkg.md index 481f24ace..fcba23beb 100644 --- a/frost-ristretto255/dkg.md +++ b/frost-ristretto255/dkg.md @@ -3,35 +3,27 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from -a `u16`. The process in which these identifiers are allocated is up to the application. - -The distributed key generation process has 3 parts, with 2 communication rounds -between them, in which each participant needs to send a "package" to every other -participant. In the first round, each participant sends the same package -(a [`round1::Package`]) to every other. In the second round, each receiver gets -their own package (a [`round2::Package`]). - -Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] -that MUST be kept secret. Between part 2 and 3, each participant needs to hold -onto a [`round2::SecretPackage`]. - -After the third part, each participant will get a [`KeyPackage`] with their -long-term secret share that must be kept secret, and a [`PublicKeyPackage`] -that is public (and will be the same between all participants). With those -they can proceed to sign messages with FROST. - +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). ## Example +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + ```rust # // ANCHOR: dkg_import -use rand::thread_rng; use std::collections::BTreeMap; use frost_ristretto255 as frost; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; @@ -48,7 +40,10 @@ let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG protocol. @@ -70,7 +65,10 @@ for participant_index in 1..=max_signers { // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). for receiver_participant_index in 1..=max_signers { if receiver_participant_index == participant_index { continue; @@ -96,7 +94,8 @@ let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG protocol. @@ -118,7 +117,8 @@ for participant_index in 1..=max_signers { // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { diff --git a/frost-ristretto255/src/keys/dkg.rs b/frost-ristretto255/src/keys/dkg.rs index 6f8b71247..2d13265bb 100644 --- a/frost-ristretto255/src/keys/dkg.rs +++ b/frost-ristretto255/src/keys/dkg.rs @@ -45,7 +45,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -55,13 +55,21 @@ pub fn part1( frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, @@ -70,14 +78,22 @@ pub fn part2( } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, diff --git a/frost-ristretto255/src/keys/refresh.rs b/frost-ristretto255/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-ristretto255/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-ristretto255/src/keys/repairable.rs b/frost-ristretto255/src/keys/repairable.rs index a303828d0..3c316bcc3 100644 --- a/frost-ristretto255/src/keys/repairable.rs +++ b/frost-ristretto255/src/keys/repairable.rs @@ -4,61 +4,67 @@ //! The RTS is used to help a signer (participant) repair their lost share. This is achieved //! using a subset of the other signers know here as `helpers`. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; +use crate::keys::{KeyPackage, PublicKeyPackage}; // This is imported separately to make `gencode` work. // (if it were below, the position of the import would vary between ciphersuites // after `cargo fmt`) -use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; use crate::{Error, Ristretto255Sha512}; -use super::{SecretShare, VerifiableSecretSharingCommitment}; +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; -/// Step 1 of RTS. +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Error> { - frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) } -/// Step 2 of RTS. -/// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// Part 2 of RTS. /// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { - frost::keys::repairable::repair_share_step_2::(deltas_j) +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) } -/// Step 3 of RTS +/// Part 3 of RTS. /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { - frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) } #[cfg(test)] mod tests { use lazy_static::lazy_static; - use rand::thread_rng; + use serde_json::Value; use crate::Ristretto255Sha512; @@ -70,32 +76,32 @@ mod tests { } #[test] - fn check_repair_share_step_1() { - let rng = thread_rng(); + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; - frost_core::tests::repairable::check_repair_share_step_1::(rng); + frost_core::tests::repairable::check_repair_share_part1::(rng); } #[test] - fn check_repair_share_step_2() { - frost_core::tests::repairable::check_repair_share_step_2::( + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::( &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_3() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_3::( + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( rng, &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_1_fails_with_invalid_min_signers() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< Ristretto255Sha512, _, >(rng); diff --git a/frost-ristretto255/src/lib.rs b/frost-ristretto255/src/lib.rs index eabb403da..490e34c87 100644 --- a/frost-ristretto255/src/lib.rs +++ b/frost-ristretto255/src/lib.rs @@ -1,8 +1,11 @@ +#![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] #![doc = include_str!("../README.md")] -use std::collections::BTreeMap; +extern crate alloc; + +use alloc::collections::BTreeMap; use curve25519_dalek::{ constants::RISTRETTO_BASEPOINT_POINT, @@ -19,8 +22,12 @@ use frost_core as frost; #[cfg(test)] mod tests; +pub mod rerandomized; + // Re-exports in our public API -pub use frost_core::{serde, Ciphersuite, Field, FieldError, Group, GroupError}; +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; pub use rand_core; /// An error. @@ -96,8 +103,11 @@ impl Group for RistrettoGroup { RISTRETTO_BASEPOINT_POINT } - fn serialize(element: &Self::Element) -> Self::Serialization { - element.compress().to_bytes() + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + Ok(element.compress().to_bytes()) } fn deserialize(buf: &Self::Serialization) -> Result { @@ -123,7 +133,7 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 64] { h.update(i); } let mut output = [0u8; 64]; - output.copy_from_slice(h.finalize().as_slice()); + output.copy_from_slice(h.finalize().as_ref()); output } @@ -134,7 +144,7 @@ fn hash_to_scalar(inputs: &[&[u8]]) -> Scalar { /// Context string from the ciphersuite in the [spec]. /// -/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-1 +/// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-1 const CONTEXT_STRING: &str = "FROST-RISTRETTO255-SHA512-v1"; /// An implementation of the FROST(ristretto255, SHA-512) ciphersuite. @@ -152,35 +162,35 @@ impl Ciphersuite for Ristretto255Sha512 { /// H1 for FROST(ristretto255, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-2.2.2.1 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho", m]) } /// H2 for FROST(ristretto255, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-2.2.2.2 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"chal", m]) } /// H3 for FROST(ristretto255, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-2.2.2.3 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce", m]) } /// H4 for FROST(ristretto255, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-2.2.2.4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-2.4.2.8 fn H4(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) } /// H5 for FROST(ristretto255, SHA-512) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.2-2.2.2.5 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.2-2.4.2.10 fn H5(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) } @@ -214,7 +224,6 @@ pub type Identifier = frost::Identifier; /// FROST(ristretto255, SHA-512) keys, key generation, key shares. pub mod keys { use super::*; - use std::collections::BTreeMap; /// The identifier list to use when generating key shares. pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, R>; @@ -305,6 +314,7 @@ pub mod keys { pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; pub mod dkg; + pub mod refresh; pub mod repairable; } @@ -397,6 +407,25 @@ pub fn aggregate( frost::aggregate(signing_package, signature_shares, pubkeys) } +/// The type of cheater detection to use. +pub type CheaterDetection = frost::CheaterDetection; + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result { + frost::aggregate_custom( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + ) +} + /// A signing key for a Schnorr signature on FROST(ristretto255, SHA-512). pub type SigningKey = frost_core::SigningKey; diff --git a/frost-ristretto255/src/rerandomized.rs b/frost-ristretto255/src/rerandomized.rs new file mode 100644 index 000000000..21410f41b --- /dev/null +++ b/frost-ristretto255/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-ristretto255/src/tests/batch.rs b/frost-ristretto255/src/tests/batch.rs index 9c08b77a7..b26d033cf 100644 --- a/frost-ristretto255/src/tests/batch.rs +++ b/frost-ristretto255/src/tests/batch.rs @@ -1,24 +1,22 @@ -use rand::thread_rng; - use crate::*; #[test] fn check_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::batch_verify::(rng); } #[test] fn check_bad_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::bad_batch_verify::(rng); } #[test] fn empty_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::empty_batch_verify::(rng); } diff --git a/frost-ristretto255/src/tests/coefficient_commitment.rs b/frost-ristretto255/src/tests/coefficient_commitment.rs index e5df3516c..a031d4c46 100644 --- a/frost-ristretto255/src/tests/coefficient_commitment.rs +++ b/frost-ristretto255/src/tests/coefficient_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,7 +12,7 @@ lazy_static! { #[test] fn check_serialization_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< Ristretto255Sha512, _, @@ -22,7 +21,7 @@ fn check_serialization_of_coefficient_commitment() { #[test] fn check_create_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< Ristretto255Sha512, _, @@ -37,7 +36,7 @@ fn check_create_coefficient_commitment_error() { #[test] fn check_get_value_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< Ristretto255Sha512, diff --git a/frost-ristretto255/src/tests/vss_commitment.rs b/frost-ristretto255/src/tests/vss_commitment.rs index d7acaaa9a..06b16e422 100644 --- a/frost-ristretto255/src/tests/vss_commitment.rs +++ b/frost-ristretto255/src/tests/vss_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,30 +12,56 @@ lazy_static! { #[test] fn check_serialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); } +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment::( rng, ); } +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::< + Ristretto255Sha512, + _, + >(rng); +} + #[test] fn check_deserialize_vss_commitment_error() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::< Ristretto255Sha512, _, >(rng, &ELEMENTS); } +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::< + Ristretto255Sha512, + _, + >(rng, &ELEMENTS); +} + #[test] fn check_compute_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_compute_public_key_package::( rng, ); diff --git a/frost-ristretto255/tests/common_traits_tests.rs b/frost-ristretto255/tests/common_traits_tests.rs index a985d0ad8..8d1fcf2b1 100644 --- a/frost-ristretto255/tests/common_traits_tests.rs +++ b/frost-ristretto255/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_ristretto255::SigningKey; use helpers::samples; -use rand::thread_rng; #[allow(clippy::unnecessary_literal_unwrap)] fn check_common_traits_for_type(v: T) { @@ -20,7 +19,7 @@ fn check_common_traits_for_type(v: #[test] fn check_signing_key_common_traits() { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; let signing_key = SigningKey::new(&mut rng); check_common_traits_for_type(signing_key); } diff --git a/frost-ristretto255/tests/helpers/samples.json b/frost-ristretto255/tests/helpers/samples.json index bb80d1b87..1fff13375 100644 --- a/frost-ristretto255/tests/helpers/samples.json +++ b/frost-ristretto255/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", "element2": "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ristretto255/tests/helpers/samples.rs b/frost-ristretto255/tests/helpers/samples.rs index ee1bfbc4f..b0bf5868b 100644 --- a/frost-ristretto255/tests/helpers/samples.rs +++ b/frost-ristretto255/tests/helpers/samples.rs @@ -36,18 +36,20 @@ fn scalar1() -> Scalar { pub fn signing_nonces() -> SigningNonces { let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); - let hiding_nonce = Nonce::deserialize(serialized_scalar1).unwrap(); - let binding_nonce = Nonce::deserialize(serialized_scalar2).unwrap(); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); SigningNonces::from_nonces(hiding_nonce, binding_nonce) } /// Generate a sample SigningCommitments. pub fn signing_commitments() -> SigningCommitments { - let serialized_element1 = ::Group::serialize(&element1()); - let serialized_element2 = ::Group::serialize(&element2()); - let hiding_nonce_commitment = NonceCommitment::deserialize(serialized_element1).unwrap(); - let binding_nonce_commitment = NonceCommitment::deserialize(serialized_element2).unwrap(); + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) } @@ -65,15 +67,15 @@ pub fn signing_package() -> SigningPackage { pub fn signature_share() -> SignatureShare { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - SignatureShare::deserialize(serialized_scalar).unwrap() + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() } /// Generate a sample SecretShare. pub fn secret_share() -> SecretShare { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); @@ -84,11 +86,11 @@ pub fn secret_share() -> SecretShare { pub fn key_package() -> KeyPackage { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) } @@ -96,38 +98,82 @@ pub fn key_package() -> KeyPackage { /// Generate a sample PublicKeyPackage. pub fn public_key_package() -> PublicKeyPackage { let identifier = 42u16.try_into().unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) } /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>() - .try_into() - .unwrap(); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + /// Generate a sample round2::Package. pub fn round2_package() -> round2::Package { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); round2::Package::new(signing_share) } diff --git a/frost-ristretto255/tests/integration_tests.rs b/frost-ristretto255/tests/integration_tests.rs index bacc9fb47..c4ef3be0e 100644 --- a/frost-ristretto255/tests/integration_tests.rs +++ b/frost-ristretto255/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_ristretto255::*; use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; #[test] @@ -10,14 +9,14 @@ fn check_zero_key_fails() { #[test] fn check_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); } #[test] fn check_dkg_part1_fails_with_invalid_signers_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -31,7 +30,7 @@ fn check_dkg_part1_fails_with_invalid_signers_min_signers() { #[test] fn check_dkg_part1_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -45,7 +44,7 @@ fn check_dkg_part1_fails_with_min_signers_greater_than_max() { #[test] fn check_dkg_part1_fails_with_invalid_signers_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -59,21 +58,82 @@ fn check_dkg_part1_fails_with_invalid_signers_max_signers() { #[test] fn check_rts() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< + Ristretto255Sha512, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Ristretto255Sha512, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Ristretto255Sha512, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::< + Ristretto255Sha512, + _, + >(rng); +} + #[test] fn check_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); } #[test] fn check_sign_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -87,7 +147,7 @@ fn check_sign_with_dealer_fails_with_invalid_min_signers() { #[test] fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -101,7 +161,7 @@ fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { #[test] fn check_sign_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -117,13 +177,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_ristretto255_sha512() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_share_generation::(rng); } #[test] fn check_share_generation_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 0; let max_signers = 3; @@ -137,7 +197,7 @@ fn check_share_generation_fails_with_invalid_min_signers() { #[test] fn check_share_generation_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -151,7 +211,7 @@ fn check_share_generation_fails_with_min_signers_greater_than_max() { #[test] fn check_share_generation_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 0; @@ -215,7 +275,7 @@ fn check_identifier_generation() -> Result<(), Error> { #[test] fn check_sign_with_dealer_and_identifiers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< Ristretto255Sha512, @@ -225,7 +285,7 @@ fn check_sign_with_dealer_and_identifiers() { #[test] fn check_sign_with_missing_identifier() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::< Ristretto255Sha512, _, @@ -234,9 +294,20 @@ fn check_sign_with_missing_identifier() { #[test] fn check_sign_with_incorrect_commitments() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::< Ristretto255Sha512, _, >(rng); } + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng) + .await; + }) + .await + .unwrap(); +} diff --git a/frost-ristretto255/tests/recreation_tests.rs b/frost-ristretto255/tests/recreation_tests.rs index c12cad7e2..0d8e51614 100644 --- a/frost-ristretto255/tests/recreation_tests.rs +++ b/frost-ristretto255/tests/recreation_tests.rs @@ -54,7 +54,7 @@ fn check_signature_share_recreation() { let encoded = signature_share.serialize(); - let new_signature_share = SignatureShare::deserialize(encoded).unwrap(); + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); assert!(signature_share == new_signature_share); } @@ -101,12 +101,51 @@ fn check_public_key_package_recreation() { let verifying_shares = public_key_package.verifying_shares(); let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); - let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); assert!(public_key_package == new_public_key_package); } +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + /// Check if round1::Package can be recreated. #[test] fn check_round1_package_recreation() { @@ -120,6 +159,28 @@ fn check_round1_package_recreation() { assert!(round1_package == new_round1_package); } +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + /// Check if round2::Package can be recreated. #[test] fn check_round2_package_recreation() { diff --git a/frost-ristretto255/tests/rerandomized_tests.rs b/frost-ristretto255/tests/rerandomized_tests.rs index a7a884c74..23277d0a9 100644 --- a/frost-ristretto255/tests/rerandomized_tests.rs +++ b/frost-ristretto255/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_ristretto255::Ristretto255Sha512; -use rand::thread_rng; #[test] fn check_randomized_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let (_msg, _group_signature, _group_pubkey) = frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); diff --git a/frost-ristretto255/tests/serde_tests.rs b/frost-ristretto255/tests/serde_tests.rs index faf1769ae..d73e846ef 100644 --- a/frost-ristretto255/tests/serde_tests.rs +++ b/frost-ristretto255/tests/serde_tests.rs @@ -434,7 +434,7 @@ fn check_key_package_serialization() { #[test] fn check_public_key_package_serialization() { - let public_key_package = samples::public_key_package(); + let public_key_package = samples::public_key_package_new(); let json = serde_json::to_string_pretty(&public_key_package).unwrap(); println!("{}", json); @@ -450,11 +450,27 @@ fn check_public_key_package_serialization() { "verifying_shares": { "2a00000000000000000000000000000000000000000000000000000000000000": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" }, - "verifying_key": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + "verifying_key": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", + "min_signers": 2 }"#; let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); assert!(public_key_package == decoded_public_key_package); + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-RISTRETTO255-SHA512-v1" + }, + "verifying_shares": { + "2a00000000000000000000000000000000000000000000000000000000000000": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + }, + "verifying_key": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + let invalid_json = "{}"; assert!(serde_json::from_str::(invalid_json).is_err()); diff --git a/frost-ristretto255/tests/serialization_tests.rs b/frost-ristretto255/tests/serialization_tests.rs index 0578a64a6..641bb4746 100644 --- a/frost-ristretto255/tests/serialization_tests.rs +++ b/frost-ristretto255/tests/serialization_tests.rs @@ -49,8 +49,11 @@ fn check_signing_package_postcard_serialization() { fn check_signature_share_postcard_serialization() { let signature_share = samples::signature_share(); let bytes = signature_share.serialize(); - assert_snapshot!(hex::encode(bytes)); - assert_eq!(signature_share, SignatureShare::deserialize(bytes).unwrap()); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); } #[test] fn check_secret_share_postcard_serialization() { @@ -79,6 +82,28 @@ fn check_public_key_package_postcard_serialization() { ); } +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round1_package_postcard_serialization() { let round1_package = samples::round1_package(); @@ -90,6 +115,17 @@ fn check_round1_package_postcard_serialization() { ); } +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round2_package_postcard_serialization() { let round2_package = samples::round2_package(); diff --git a/frost-ristretto255/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-ristretto255/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..8a5fd3195 --- /dev/null +++ b/frost-ristretto255/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ristretto255/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00d76ecff5012a00000000000000000000000000000000000000000000000000000000000000e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d760102 diff --git a/frost-ristretto255/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-ristretto255/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..9c27582cc --- /dev/null +++ b/frost-ristretto255/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ristretto255/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a0000000000000000000000000000000000000000000000000000000000000002498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a01e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d760203 diff --git a/frost-ristretto255/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-ristretto255/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..6019d5add --- /dev/null +++ b/frost-ristretto255/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-ristretto255/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +2a0000000000000000000000000000000000000000000000000000000000000001e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a0203 diff --git a/frost-secp256k1-tr/CHANGELOG.md b/frost-secp256k1-tr/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-secp256k1-tr/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-secp256k1-tr/Cargo.toml b/frost-secp256k1-tr/Cargo.toml new file mode 100644 index 000000000..ad45c8fb9 --- /dev/null +++ b/frost-secp256k1-tr/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "frost-secp256k1-tr" +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true +readme = "README.md" +license.workspace = true +repository.workspace = true +categories.workspace = true +keywords = ["cryptography", "crypto", "secp256k1", "threshold", "signature"] +description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST and Taproot." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +document-features.workspace = true +frost-core.workspace = true +frost-rerandomized.workspace = true +k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"], default-features = false } +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } + +[dev-dependencies] +criterion.workspace = true +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +insta.workspace = true +hex.workspace = true +lazy_static.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +secp256k1 = "0.31.0" +serde_json.workspace = true +tokio.workspace = true + +[features] +default = ["serialization"] +#! ## Features +## Enable `serde` support for types that need to be communicated. You +## can use `serde` to serialize structs with any encoder that supports +## `serde` (e.g. JSON with `serde_json`). +serde = ["frost-core/serde"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] + +[lib] +# Disables non-criterion benchmark which is not used; prevents errors +# when using criterion-specific flags +bench = false + +[[bench]] +name = "bench" +harness = false diff --git a/frost-secp256k1-tr/README.md b/frost-secp256k1-tr/README.md new file mode 100644 index 000000000..bc74f3ef2 --- /dev/null +++ b/frost-secp256k1-tr/README.md @@ -0,0 +1,127 @@ +An implementation of Schnorr signatures on the secp256k1 curve (Taproot) for both single and threshold numbers +of signers (FROST). + +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +secp256k1 curve (Taproot). For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + + + +## Example: key generation with trusted dealer and FROST signing + +Creating a key with a trusted dealer and splitting into shares; then signing a message +and aggregating the signature. Note that the example just simulates a distributed +scenario in a single thread and it abstracts away any communication between peers. + + +```rust +# // ANCHOR: tkg_gen +use frost_secp256k1_tr as frost; +use std::collections::BTreeMap; + +let mut rng = rand::rngs::OsRng; +let max_signers = 5; +let min_signers = 3; +let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, +)?; +# // ANCHOR_END: tkg_gen + +// Verifies the secret shares from the dealer and store them in a BTreeMap. +// In practice, the KeyPackages must be sent to its respective participants +// through a confidential and authenticated channel. +let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + +for (identifier, secret_share) in shares { + # // ANCHOR: tkg_verify + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + # // ANCHOR_END: tkg_verify + key_packages.insert(identifier, key_package); +} + +let mut nonces_map = BTreeMap::new(); +let mut commitments_map = BTreeMap::new(); + +//////////////////////////////////////////////////////////////////////////// +// Round 1: generating nonces and signing commitments for each participant +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( + key_package.signing_share(), + &mut rng, + ); + # // ANCHOR_END: round1_commit + // In practice, the nonces must be kept by the participant to use in the + // next round, while the commitment must be sent to the coordinator + // (or to every other participant if there is no coordinator) using + // an authenticated channel. + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); +} + +// This is what the signature aggregator / coordinator needs to do: +// - decide what message to sign +// - take one (unused) commitment per signing participant +let mut signature_shares = BTreeMap::new(); +# // ANCHOR: round2_package +let message = "message to sign".as_bytes(); +# // In practice, the SigningPackage must be sent to all participants +# // involved in the current signing (at least min_signers participants), +# // using an authenticate channel (and confidential if the message is secret). +let signing_package = frost::SigningPackage::new(commitments_map, message); +# // ANCHOR_END: round2_package + +//////////////////////////////////////////////////////////////////////////// +// Round 2: each participant generates their signature share +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + + let nonces = &nonces_map[participant_identifier]; + + // Each participant generates their signature share. + # // ANCHOR: round2_sign + let signature_share = frost::round2::sign(&signing_package, nonces, key_package)?; + # // ANCHOR_END: round2_sign + + // In practice, the signature share must be sent to the Coordinator + // using an authenticated channel. + signature_shares.insert(*participant_identifier, signature_share); +} + +//////////////////////////////////////////////////////////////////////////// +// Aggregation: collects the signing shares from all participants, +// generates the final signature. +//////////////////////////////////////////////////////////////////////////// + +// Aggregate (also verifies the signature shares) +# // ANCHOR: aggregate +let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?; +# // ANCHOR_END: aggregate + + +// Check that the threshold signature can be verified by the group public +// key (the verification key). +# // ANCHOR: verify +let is_signature_valid = pubkey_package + .verifying_key() + .verify(message, &group_signature) + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); + +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/benches/bench.rs b/frost-secp256k1-tr/benches/bench.rs new file mode 100644 index 000000000..d2ce56f2a --- /dev/null +++ b/frost-secp256k1-tr/benches/bench.rs @@ -0,0 +1,18 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use frost_secp256k1_tr::*; + +fn bench_secp256k1_batch_verify(c: &mut Criterion) { + let mut rng = rand::rngs::OsRng; + + frost_core::benches::bench_batch_verify::(c, "secp256k1", &mut rng); +} + +fn bench_secp256k1_sign(c: &mut Criterion) { + let mut rng = rand::rngs::OsRng; + + frost_core::benches::bench_sign::(c, "secp256k1", &mut rng); +} + +criterion_group!(benches, bench_secp256k1_batch_verify, bench_secp256k1_sign); +criterion_main!(benches); diff --git a/frost-secp256k1-tr/dkg.md b/frost-secp256k1-tr/dkg.md new file mode 100644 index 000000000..e0be3936c --- /dev/null +++ b/frost-secp256k1-tr/dkg.md @@ -0,0 +1,168 @@ +# Distributed Key Generation (DKG) + +The DKG module supports generating FROST key shares in a distributed manner, +without a trusted dealer. + +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). + +## Example + +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + +```rust +# // ANCHOR: dkg_import +use std::collections::BTreeMap; + +use frost_secp256k1_tr as frost; + +let mut rng = rand::rngs::OsRng; + +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 1 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 1 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round1_secret_packages = BTreeMap::new(); + +// Keep track of all round 1 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). +let mut received_round1_packages = BTreeMap::new(); + +// For each participant, perform the first part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + # // ANCHOR: dkg_part1 + let (round1_secret_package, round1_package) = frost::keys::dkg::part1( + participant_identifier, + max_signers, + min_signers, + &mut rng, + )?; + # // ANCHOR_END: dkg_part1 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round1_secret_packages.insert(participant_identifier, round1_secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + for receiver_participant_index in 1..=max_signers { + if receiver_participant_index == participant_index { + continue; + } + let receiver_participant_identifier: frost::Identifier = receiver_participant_index + .try_into() + .expect("should be nonzero"); + received_round1_packages + .entry(receiver_participant_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round1_package.clone()); + } +} + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 2 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 2 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round2_secret_packages = BTreeMap::new(); + +// Keep track of all round 2 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). +let mut received_round2_packages = BTreeMap::new(); + +// For each participant, perform the second part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + # // ANCHOR: dkg_part2 + let (round2_secret_package, round2_packages) = + frost::keys::dkg::part2(round1_secret_package, round1_packages)?; + # // ANCHOR_END: dkg_part2 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round2_secret_packages.insert(participant_identifier, round2_secret_package); + + // "Send" the round 2 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + // Note that, in contrast to the previous part, here each other participant + // gets its own specific package. + for (receiver_identifier, round2_package) in round2_packages { + received_round2_packages + .entry(receiver_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round2_package); + } +} + +//////////////////////////////////////////////////////////////////////////// +// Key generation, final computation +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's long-lived key package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut key_packages = BTreeMap::new(); + +// Keep track of each participant's public key package. +// In practice, if there is a Coordinator, only they need to store the set. +// If there is not, then all candidates must store their own sets. +// All participants will have the same exact public key package. +let mut pubkey_packages = BTreeMap::new(); + +// For each participant, perform the third part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round2_secret_package = &round2_secret_packages[&participant_identifier]; + let round1_packages = &received_round1_packages[&participant_identifier]; + let round2_packages = &received_round2_packages[&participant_identifier]; + # // ANCHOR: dkg_part3 + let (key_package, pubkey_package) = frost::keys::dkg::part3( + round2_secret_package, + round1_packages, + round2_packages, + )?; + # // ANCHOR_END: dkg_part3 + key_packages.insert(participant_identifier, key_package); + pubkey_packages.insert(participant_identifier, pubkey_package); +} + +// With its own key package and the pubkey package, each participant can now proceed +// to sign with FROST. +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/src/keys/dkg.rs b/frost-secp256k1-tr/src/keys/dkg.rs new file mode 100644 index 000000000..9ea40b263 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/dkg.rs @@ -0,0 +1,103 @@ +#![doc = include_str!("../../dkg.md")] +use super::*; + +/// DKG Round 1 structures. +pub mod round1 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the first and second parts of the DKG protocol (round 1). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round1::SecretPackage; + + /// The package that must be broadcast by each participant to all other participants + /// between the first and second parts of the DKG protocol (round 1). + pub type Package = frost::keys::dkg::round1::Package; +} + +/// DKG Round 2 structures. +pub mod round2 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the second and third parts of the DKG protocol (round 2). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round2::SecretPackage; + + /// A package that must be sent by each participant to some other participants + /// in Round 2 of the DKG protocol. Note that there is one specific package + /// for each specific recipient, in contrast to Round 1. + /// + /// # Security + /// + /// The package must be sent on an *confidential* and *authenticated* channel. + pub type Package = frost::keys::dkg::round2::Package; +} + +/// Performs the first part of the distributed key generation protocol +/// for the given participant. +/// +/// It returns the [`round1::SecretPackage`] that must be kept in memory +/// by the participant for the other steps, and the [`round1::Package`] that +/// must be sent to each other participant in the DKG run. +pub fn part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. +/// +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. +pub fn part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::dkg::part2(secret_package, round1_packages) +} + +/// Performs the third and final part of the distributed key generation protocol +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`KeyPackage`] that has the long-lived key share for the +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. +pub fn part3( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages) +} diff --git a/frost-secp256k1-tr/src/keys/refresh.rs b/frost-secp256k1-tr/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-secp256k1-tr/src/keys/repairable.rs b/frost-secp256k1-tr/src/keys/repairable.rs new file mode 100644 index 000000000..3ae930d49 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/repairable.rs @@ -0,0 +1,107 @@ +//! Repairable Threshold Scheme +//! +//! Implements the Repairable Threshold Scheme (RTS) from . +//! The RTS is used to help a signer (participant) repair their lost share. This is achieved +//! using a subset of the other signers know here as `helpers`. + +use alloc::collections::BTreeMap; + +use crate::keys::{KeyPackage, PublicKeyPackage}; +// This is imported separately to make `gencode` work. +// (if it were below, the position of the import would vary between ciphersuites +// after `cargo fmt`) +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; +use crate::{Error, Secp256K1Sha256TR}; + +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; + +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. +/// +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. +/// +/// Returns a BTreeMap mapping which value should be sent to which participant. +pub fn repair_share_part1( + helpers: &[Identifier], + key_package_i: &KeyPackage, + rng: &mut R, + participant: Identifier, +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) +} + +/// Part 2 of RTS. +/// +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) +} + +/// Part 3 of RTS. +/// +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], + identifier: Identifier, + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) +} + +#[cfg(test)] +mod tests { + + use lazy_static::lazy_static; + + use serde_json::Value; + + use crate::Secp256K1Sha256TR; + + lazy_static! { + pub static ref REPAIR_SHARE: Value = + serde_json::from_str(include_str!("../../tests/helpers/repair-share.json").trim()) + .unwrap(); + } + + #[test] + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; + + frost_core::tests::repairable::check_repair_share_part1::(rng); + } + + #[test] + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::(&REPAIR_SHARE); + } + + #[test] + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( + rng, + &REPAIR_SHARE, + ); + } + + #[test] + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< + Secp256K1Sha256TR, + _, + >(rng); + } +} diff --git a/frost-secp256k1-tr/src/lib.rs b/frost-secp256k1-tr/src/lib.rs new file mode 100644 index 000000000..daa19608e --- /dev/null +++ b/frost-secp256k1-tr/src/lib.rs @@ -0,0 +1,917 @@ +#![no_std] +#![allow(non_snake_case)] +#![deny(missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![doc = document_features::document_features!()] + +extern crate alloc; + +use alloc::vec; +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; + +use frost_rerandomized::RandomizedCiphersuite; +use k256::elliptic_curve::ops::Reduce; +use k256::{ + elliptic_curve::{ + bigint::U256, + group::prime::PrimeCurveAffine, + hash2curve::{hash_to_field, ExpandMsgXmd}, + point::AffineCoordinates, + sec1::{FromEncodedPoint, ToEncodedPoint}, + Field as FFField, PrimeField, + }, + AffinePoint, ProjectivePoint, Scalar, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use frost_core::{self as frost, random_nonzero}; + +use keys::EvenY; +use keys::Tweak; + +#[cfg(test)] +mod tests; + +// Re-exports in our public API +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{ + Challenge, Ciphersuite, Element, Field, FieldError, Group, GroupCommitment, GroupError, +}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct Secp256K1ScalarField; + +impl Field for Secp256K1ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`Scalar`]'s Eq/PartialEq does a constant-time comparison + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert().unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let field_bytes: &k256::FieldBytes = buf.into(); + match Scalar::from_repr(*field_bytes).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + let mut array = Self::serialize(scalar); + array.reverse(); + array + } +} + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Secp256K1Group; + +impl Group for Secp256K1Group { + type Field = Secp256K1ScalarField; + + type Element = ProjectivePoint; + + /// [SEC 1][1] serialization of a compressed point in secp256k1 takes 33 bytes + /// (1-byte prefix and 32 bytes for the coordinate). + /// + /// Note that, in the SEC 1 spec, the identity is encoded as a single null byte; + /// but here we pad with zeroes. This is acceptable as the identity _should_ never + /// be serialized in FROST, else we error. + /// + /// [1]: https://secg.org/sec1-v2.pdf + type Serialization = [u8; 33]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + ProjectivePoint::IDENTITY + } + + fn generator() -> Self::Element { + ProjectivePoint::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + let mut fixed_serialized = [0; 33]; + let serialized_point = element.to_affine().to_encoded_point(true); + let serialized = serialized_point.as_bytes(); + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let encoded_point = + k256::EncodedPoint::from_bytes(buf).map_err(|_| GroupError::MalformedElement)?; + + match Option::::from(AffinePoint::from_encoded_point(&encoded_point)) { + Some(point) => { + if point.is_identity().into() { + // This is actually impossible since the identity is encoded a a single byte + // which will never happen since we receive a 33-byte buffer. + // We leave the check for consistency. + Err(GroupError::InvalidIdentityElement) + } else { + Ok(ProjectivePoint::from(point)) + } + } + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for i in inputs { + h.update(i); + } + let mut output = [0u8; 32]; + output.copy_from_slice(h.finalize().as_ref()); + output +} + +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { + let mut u = [Secp256K1ScalarField::zero()]; + hash_to_field::, Scalar>(&[msg], domain, &mut u) + .expect("should never return error according to error cases described in ExpandMsgXmd"); + u[0] +} + +/// Context string from the ciphersuite in the [spec]. +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-1 +const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-TR-v1"; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Secp256K1Sha256TR; + +/// Digest the hasher to a Scalar +fn hasher_to_scalar(hasher: Sha256) -> Scalar { + // This is acceptable because secp256k1 curve order is close to 2^256, + // and the input is uniformly random since it is a hash output, therefore + // the bias is negligibly small. + Scalar::reduce(U256::from_be_slice(&hasher.finalize())) +} + +/// Create a BIP340 compliant tagged hash +fn tagged_hash(tag: &str) -> Sha256 { + let mut hasher = Sha256::new(); + let mut tag_hasher = Sha256::new(); + tag_hasher.update(tag.as_bytes()); + let tag_hash = tag_hasher.finalize(); + hasher.update(tag_hash); + hasher.update(tag_hash); + hasher +} + +/// Create a BIP341 compliant taproot tweak +fn tweak>( + public_key: &<::Group as Group>::Element, + merkle_root: Option, +) -> Scalar { + match merkle_root { + None => { + let mut hasher = tagged_hash("TapTweak"); + hasher.update(public_key.to_affine().x()); + hasher_to_scalar(hasher) + } + Some(root) => { + let mut hasher = tagged_hash("TapTweak"); + hasher.update(public_key.to_affine().x()); + hasher.update(root.as_ref()); + hasher_to_scalar(hasher) + } + } +} + +// Negate a Nonce +fn negate_nonce(nonce: &frost_core::round1::Nonce) -> frost_core::round1::Nonce { + frost_core::round1::Nonce::::from_scalar(-nonce.to_scalar()) +} + +// Negate a SigningNonces +fn negate_nonces(signing_nonces: &round1::SigningNonces) -> round1::SigningNonces { + // TODO: this recomputes commitments which is expensive, and not needed. + // Create an `internals` SigningNonces::from_nonces_and_commitments or + // something similar. + round1::SigningNonces::from_nonces( + negate_nonce(signing_nonces.hiding()), + negate_nonce(signing_nonces.binding()), + ) +} + +impl Ciphersuite for Secp256K1Sha256TR { + const ID: &'static str = CONTEXT_STRING; + + type Group = Secp256K1Group; + + type HashOutput = [u8; 32]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) + } + + /// H2 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + let mut hasher = tagged_hash("BIP0340/challenge"); + hasher.update(m); + hasher_to_scalar(hasher) + } + + /// H3 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) + } + + /// H4 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(secp256k1, SHA-256) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) + } + + /// HID for FROST(secp256k1, SHA-256) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) + } + + // Sign, negating the key if required by BIP-340. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + let signing_key = signing_key.clone().into_even_y(None); + signing_key.default_sign(rng, message) + } + + // Preprocess sign inputs, negating the keys in the KeyPackage if required + // by BIP-340. + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a keys::KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, keys::KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Owned(key_package.clone().into_even_y(None)), + )) + } + + // Preprocess sign inputs, negating the keys in the PublicKeyPackage if + // required by BIP-340. + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, + public_key_package: &'a keys::PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap>, + Cow<'a, keys::PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Owned(public_key_package.clone().into_even_y(None)), + )) + } + + // Preprocess verify inputs, negating the VerifyingKey and `signature.R` if required by + // BIP-340. + fn pre_verify<'a>( + message: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result<(Cow<'a, [u8]>, Cow<'a, Signature>, Cow<'a, VerifyingKey>), Error> { + let public_key = public_key.into_even_y(None); + let signature = signature.into_even_y(None); + Ok(( + Cow::Borrowed(message), + Cow::Owned(signature), + Cow::Owned(public_key), + )) + } + + // Generate a nonce, negating it if required by BIP-340. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + if R.to_affine().y_is_odd().into() { + (-k, -R) + } else { + (k, R) + } + } + + // Compute the challenge. Per BIP-340, only the X coordinate of R and + // verifying_key are hashed, unlike vanilla FROST. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + let mut preimage = vec![]; + preimage.extend_from_slice(&R.to_affine().x()); + preimage.extend_from_slice(&verifying_key.to_element().to_affine().x()); + preimage.extend_from_slice(message); + Ok(Challenge::from_scalar(S::H2(&preimage[..]))) + } + + /// Compute a signature share, negating the nonces if required by BIP-340. + fn compute_signature_share( + group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: frost::BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &frost::keys::KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + let signer_nonces = if !group_commitment.has_even_y() { + negate_nonces(signer_nonces) + } else { + signer_nonces.clone() + }; + + frost::round2::compute_signature_share( + &signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Verify a signature share, negating the group commitment share if + /// required by BIP-340. + fn verify_share( + group_commitment: &GroupCommitment, + signature_share: &frost_core::round2::SignatureShare, + identifier: Identifier, + group_commitment_share: &frost_core::round1::GroupCommitmentShare, + verifying_share: &frost_core::keys::VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + let group_commitment_share = if !group_commitment.has_even_y() { + frost_core::round1::GroupCommitmentShare::from_element( + -group_commitment_share.to_element(), + ) + } else { + *group_commitment_share + }; + signature_share.verify( + identifier, + &group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Serialize a signature in compact BIP340 format, with an x-only R point. + fn serialize_signature(signature: &Signature) -> Result, Error> { + let R_bytes = Self::Group::serialize(signature.R())?; + let z_bytes = ::Field::serialize(signature.z()); + + let mut bytes = vec![0u8; 64]; + bytes[..32].copy_from_slice(&R_bytes[1..]); + bytes[32..].copy_from_slice(&z_bytes); + Ok(bytes) + } + + /// Deserialize a signature in compact BIP340 format, with an x-only R point. + fn deserialize_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(Error::MalformedSignature); + } + + let mut R_bytes = [0u8; 33]; + R_bytes[0] = 0x02; // taproot signatures always have an even R point + R_bytes[1..].copy_from_slice(&bytes[..32]); + + let mut z_bytes = [0u8; 32]; + z_bytes.copy_from_slice(&bytes[32..]); + + let R = Self::Group::deserialize(&R_bytes)?; + let z = ::Field::deserialize(&z_bytes)?; + + Ok(Signature::new(R, z)) + } + + /// Post-process the DKG output. We add an unusable taproot tweak to the + /// group key computed by a DKG run, to prevent peers from inserting rogue + /// tapscript tweaks into the group's joint public key. + fn post_dkg( + key_package: keys::KeyPackage, + public_key_package: keys::PublicKeyPackage, + ) -> Result<(keys::KeyPackage, keys::PublicKeyPackage), Error> { + // From BIP-341: + // > If the spending conditions do not require a script path, the output + // > key should commit to an unspendable script path instead of having + // > no script path. This can be achieved by computing the output key + // > point as Q = P + int(hashTapTweak(bytes(P)))G. + Ok(( + key_package.tweak::<&[u8]>(None), + public_key_package.tweak::<&[u8]>(None), + )) + } +} + +impl RandomizedCiphersuite for Secp256K1Sha256TR { + fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar( + &[CONTEXT_STRING.as_bytes(), b"randomizer"], + m, + )) + } +} + +type S = Secp256K1Sha256TR; + +/// A FROST(secp256k1, SHA-256) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(secp256k1, SHA-256) keys, key generation, key shares. +pub mod keys { + use super::*; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, S>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(secp256k1, SHA-256) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare; + + /// A FROST(secp256k1, SHA-256) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; + + /// Trait for ensuring the group public key has an even Y coordinate. + /// + /// In BIP-340, public keys are encoded with only the X coordinate, which + /// means that two Y coordinates are possible. The specification says that + /// the coordinate which is even must be used. Alternatively, something + /// equivalent can be accomplished by simply converting any existing + /// (non-encoded) public key to have an even Y coordinate. + /// + /// This trait is used to enable this procedure, by changing the private and + /// public keys to ensure that the public key has a even Y coordinate. This + /// is done by simply negating both keys if Y is even (in a field, negating + /// is equivalent to computing p - x where p is the prime modulus. Since p + /// is odd, if x is odd then the result will be even). Fortunately this + /// works even after Shamir secret sharing, in the individual signing and + /// verifying shares, since it's linear. + pub trait EvenY { + /// Return if the given type has a group public key with an even Y + /// coordinate. + fn has_even_y(&self) -> bool; + + /// Convert the given type to make sure the group public key has an even + /// Y coordinate. `is_even` can be specified if evenness was already + /// determined beforehand. + fn into_even_y(self, is_even: Option) -> Self; + } + + impl EvenY for PublicKeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate verifying key + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = self + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(-vs.to_element()); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new_internal(verifying_shares, verifying_key, self.min_signers()) + } else { + self + } + } + } + + impl EvenY for KeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate all components + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + let signing_share = SigningShare::new(-self.signing_share().to_scalar()); + let verifying_share = VerifyingShare::new(-self.verifying_share().to_element()); + KeyPackage::new( + *self.identifier(), + signing_share, + verifying_share, + verifying_key, + *self.min_signers(), + ) + } else { + self + } + } + } + + impl EvenY for VerifyingKey { + fn has_even_y(&self) -> bool { + (!self.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + VerifyingKey::new(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for GroupCommitment { + fn has_even_y(&self) -> bool { + (!self.clone().to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::from_element(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for Signature { + fn has_even_y(&self) -> bool { + (!self.R().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::new(-*self.R(), *self.z()) + } else { + self + } + } + } + + impl EvenY for SigningKey { + fn has_even_y(&self) -> bool { + (!Into::::into(self) + .to_element() + .to_affine() + .y_is_odd()) + .into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + SigningKey::from_scalar(-self.to_scalar()) + .expect("the original SigningKey must be nonzero") + } else { + self + } + } + } + + /// Trait for tweaking a key component following BIP-341 + pub trait Tweak: EvenY { + /// Convert the given type to add a tweak. + fn tweak>(self, merkle_root: Option) -> Self; + } + + impl Tweak for PublicKeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let public_key_package = self.into_even_y(None); + let verifying_key = + VerifyingKey::new(public_key_package.verifying_key().to_element() + tp); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = public_key_package + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(vs.to_element() + tp); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new_internal( + verifying_shares, + verifying_key, + public_key_package.min_signers(), + ) + } + } + + impl Tweak for KeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let key_package = self.into_even_y(None); + let verifying_key = VerifyingKey::new(key_package.verifying_key().to_element() + tp); + let signing_share = SigningShare::new(key_package.signing_share().to_scalar() + t); + let verifying_share = + VerifyingShare::new(key_package.verifying_share().to_element() + tp); + KeyPackage::new( + *key_package.identifier(), + signing_share, + verifying_share, + verifying_key, + *key_package.min_signers(), + ) + } + } + + pub mod dkg; + pub mod refresh; + pub mod repairable; +} + +/// FROST(secp256k1, SHA-256) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(secp256k1, SHA-256) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(secp256k1, SHA-256) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use keys::Tweak; + + use super::*; + + /// A FROST(secp256k1, SHA-256) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare; + + /// Performed once by each participant selected for the signing operation. + /// + /// Receives the message to be signed and a set of signing commitments and a set + /// of randomizing commitments to be used in that signing operation, including + /// that for this participant. + /// + /// Assumes the participant has already determined which nonce corresponds with + /// the commitment that was assigned by the coordinator in the SigningPackage. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + + /// Same as [`sign()`], but using a Taproot tweak as specified in BIP-341. + pub fn sign_with_tweak( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + merkle_root: Option<&[u8]>, + ) -> Result { + let key_package = key_package.clone().tweak(merkle_root); + frost::round2::sign(signing_package, signer_nonces, &key_package) + } +} + +/// A Schnorr signature on FROST(secp256k1, SHA-256). +pub type Signature = frost_core::Signature; + +/// Verifies each FROST(secp256k1, SHA-256) participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain Schnorr +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, public_key_package) +} + +/// Same as [`aggregate()`], but using a Taproot tweak as specified in BIP-341. +pub fn aggregate_with_tweak( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, + merkle_root: Option<&[u8]>, +) -> Result { + let public_key_package = public_key_package.clone().tweak(merkle_root); + frost::aggregate(signing_package, signature_shares, &public_key_package) +} + +/// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256). +pub type SigningKey = frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(secp256k1, SHA-256). +pub type VerifyingKey = frost_core::VerifyingKey; diff --git a/frost-secp256k1-tr/src/rerandomized.rs b/frost-secp256k1-tr/src/rerandomized.rs new file mode 100644 index 000000000..7135cdbd4 --- /dev/null +++ b/frost-secp256k1-tr/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-secp256k1-tr/src/tests.rs b/frost-secp256k1-tr/src/tests.rs new file mode 100644 index 000000000..15a3e184f --- /dev/null +++ b/frost-secp256k1-tr/src/tests.rs @@ -0,0 +1,5 @@ +mod batch; +mod coefficient_commitment; +mod deserialize; +mod proptests; +mod vss_commitment; diff --git a/frost-secp256k1-tr/src/tests/batch.rs b/frost-secp256k1-tr/src/tests/batch.rs new file mode 100644 index 000000000..d22efdf5d --- /dev/null +++ b/frost-secp256k1-tr/src/tests/batch.rs @@ -0,0 +1,22 @@ +use crate::*; + +#[test] +fn check_batch_verify() { + let rng = rand::rngs::OsRng; + + frost_core::tests::batch::batch_verify::(rng); +} + +#[test] +fn check_bad_batch_verify() { + let rng = rand::rngs::OsRng; + + frost_core::tests::batch::bad_batch_verify::(rng); +} + +#[test] +fn empty_batch_verify() { + let rng = rand::rngs::OsRng; + + frost_core::tests::batch::empty_batch_verify::(rng); +} diff --git a/frost-secp256k1-tr/src/tests/coefficient_commitment.rs b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs new file mode 100644 index 000000000..71706ea62 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs @@ -0,0 +1,45 @@ +use lazy_static::lazy_static; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization of CoefficientCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialization_of_coefficient_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_create_coefficient_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} +#[test] +fn check_create_coefficient_commitment_error() { + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment_error::< + Secp256K1Sha256TR, + >(&ELEMENTS); +} + +#[test] +fn check_get_value_of_coefficient_commitment() { + let rng = rand::rngs::OsRng; + + frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} diff --git a/frost-secp256k1-tr/src/tests/deserialize.rs b/frost-secp256k1-tr/src/tests/deserialize.rs new file mode 100644 index 000000000..7d4c630b6 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/deserialize.rs @@ -0,0 +1,38 @@ +use crate::*; + +#[test] +fn check_deserialize_non_canonical() { + let mut encoded_generator = ::Group::serialize( + &::Group::generator(), + ) + .unwrap(); + + let r = ::Group::deserialize(&encoded_generator); + assert!(r.is_ok()); + + // The first byte should be 0x02 or 0x03. Set other value to + // create a non-canonical encoding. + encoded_generator[0] = 0xFF; + let r = ::Group::deserialize(&encoded_generator); + assert_eq!(r, Err(GroupError::MalformedElement)); + + // Besides the first byte, it is still possible to get non-canonical encodings. + // This is x = p + 2 which is non-canonical and maps to a valid prime-order point. + let encoded_point = + hex::decode("02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc31") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::MalformedElement)); +} + +#[test] +fn check_deserialize_identity() { + // The identity is actually encoded as a single byte; but the API does not + // allow us to change that. Try to send something similar. + let encoded_identity = [0u8; 33]; + + let r = ::Group::deserialize(&encoded_identity); + assert_eq!(r, Err(GroupError::MalformedElement)); +} diff --git a/frost-secp256k1-tr/src/tests/proptests.rs b/frost-secp256k1-tr/src/tests/proptests.rs new file mode 100644 index 000000000..cad88c330 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/proptests.rs @@ -0,0 +1,33 @@ +use crate::*; +use frost_core::tests::proptests::{tweak_strategy, SignatureCase}; +use proptest::prelude::*; + +use rand_chacha::ChaChaRng; +use rand_core::SeedableRng; + +proptest! { + + #[test] + fn tweak_signature( + tweaks in prop::collection::vec(tweak_strategy(), (0,5)), + rng_seed in prop::array::uniform32(any::()), + ) { + // Use a deterministic RNG so that test failures can be reproduced. + // Seeding with 64 bits of entropy is INSECURE and this code should + // not be copied outside of this test! + let rng = ChaChaRng::from_seed(rng_seed); + + // Create a test case for each signature type. + let msg = b"test message for proptests"; + let mut sig = SignatureCase::::new(rng, msg.to_vec()); + + // Apply tweaks to each case. + for t in &tweaks { + sig.apply_tweak(t); + } + + assert!(sig.check()); + } + + +} diff --git a/frost-secp256k1-tr/src/tests/vss_commitment.rs b/frost-secp256k1-tr/src/tests/vss_commitment.rs new file mode 100644 index 000000000..f264c3328 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/vss_commitment.rs @@ -0,0 +1,66 @@ +use lazy_static::lazy_static; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization VerifiableSecretSharingCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialize_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); +} + +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::( + rng, + ); +} + +#[test] +fn check_deserialize_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_vss_commitment::( + rng, + ); +} + +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::( + rng, + ); +} + +#[test] +fn check_deserialize_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( + rng, &ELEMENTS, + ); +} + +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::< + Secp256K1Sha256TR, + _, + >(rng, &ELEMENTS); +} + +#[test] +fn check_compute_public_key_package() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_compute_public_key_package::( + rng, + ); +} diff --git a/frost-secp256k1-tr/tests/common_traits_tests.rs b/frost-secp256k1-tr/tests/common_traits_tests.rs new file mode 100644 index 000000000..93265b7a8 --- /dev/null +++ b/frost-secp256k1-tr/tests/common_traits_tests.rs @@ -0,0 +1,73 @@ +#![cfg(feature = "serde")] + +mod helpers; + +use frost_secp256k1_tr::SigningKey; +use helpers::samples; + +#[allow(clippy::unnecessary_literal_unwrap)] +fn check_common_traits_for_type(v: T) { + // Make sure can be debug-printed. This also catches if the Debug does not + // have an endless recursion (a popular mistake). + println!("{:?}", v); + // Test Clone and Eq + assert_eq!(v, v.clone()); + // Make sure it can be unwrapped in a Result (which requires Debug). + let e: Result = Ok(v.clone()); + assert_eq!(v, e.unwrap()); +} + +#[test] +fn check_signing_key_common_traits() { + let mut rng = rand::rngs::OsRng; + let signing_key = SigningKey::new(&mut rng); + check_common_traits_for_type(signing_key); +} + +#[test] +fn check_signing_commitments_common_traits() { + let commitments = samples::signing_commitments(); + check_common_traits_for_type(commitments); +} + +#[test] +fn check_signing_package_common_traits() { + let signing_package = samples::signing_package(); + check_common_traits_for_type(signing_package); +} + +#[test] +fn check_signature_share_common_traits() { + let signature_share = samples::signature_share(); + check_common_traits_for_type(signature_share); +} + +#[test] +fn check_secret_share_common_traits() { + let secret_share = samples::secret_share(); + check_common_traits_for_type(secret_share); +} + +#[test] +fn check_key_package_common_traits() { + let key_package = samples::key_package(); + check_common_traits_for_type(key_package); +} + +#[test] +fn check_public_key_package_common_traits() { + let public_key_package = samples::public_key_package(); + check_common_traits_for_type(public_key_package); +} + +#[test] +fn check_round1_package_common_traits() { + let round1_package = samples::round1_package(); + check_common_traits_for_type(round1_package); +} + +#[test] +fn check_round2_package_common_traits() { + let round2_package = samples::round2_package(); + check_common_traits_for_type(round2_package); +} diff --git a/frost-secp256k1-tr/tests/helpers/elements.json b/frost-secp256k1-tr/tests/helpers/elements.json new file mode 100644 index 000000000..e8cd4082d --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/elements.json @@ -0,0 +1,5 @@ +{ + "elements": { + "invalid_element": "123456afdf4a7f88885ab26b20d18edb7d4d9589812a6cf1a5a1a09d3808dae5d8" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/mod.rs b/frost-secp256k1-tr/tests/helpers/mod.rs new file mode 100644 index 000000000..299e33f82 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/mod.rs @@ -0,0 +1,24 @@ +// Required since each integration test is compiled as a separated crate, +// and each one uses only part of the module. +#![allow(dead_code)] + +use frost_secp256k1_tr::Secp256K1Sha256TR; +use secp256k1::Secp256k1; + +pub mod samples; + +pub fn verify_signature( + msg: &[u8], + group_signature: &frost_core::Signature, + group_pubkey: &frost_core::VerifyingKey, +) { + let secp = Secp256k1::new(); + let sig = secp256k1::schnorr::Signature::from_byte_array( + group_signature.serialize().unwrap().try_into().unwrap(), + ); + let pubkey = secp256k1::XOnlyPublicKey::from_byte_array( + group_pubkey.serialize().unwrap()[1..33].try_into().unwrap(), + ) + .unwrap(); + secp.verify_schnorr(&sig, msg, &pubkey).unwrap(); +} diff --git a/frost-secp256k1-tr/tests/helpers/repair-share.json b/frost-secp256k1-tr/tests/helpers/repair-share.json new file mode 100644 index 000000000..f4db9bc84 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/repair-share.json @@ -0,0 +1,15 @@ +{ + "scalar_generation": { + "random_scalar_1": "1847f6c4a85096e5dbc9e200c9691c5164f8e276d32d4a54ebaf4275474a1403", + "random_scalar_2": "eac5595269d108812eaa865bf62c703a2c128a61fa3bd4dc837b9314bc515204", + "random_scalar_3": "5b3b6084e41c273a39a8d9bbbd87fbcd626c07030142bf78c6c91247bf175700", + "random_scalar_sum": "5e48b09bf63dc6a1441d42187d1d885a38c896f51f633e6e76218944f27c7bc6" + }, + "sigma_generation": { + "sigma_1": "ec3aa83140065181d75b746bfd6bbbbaf212bdfbb3a91670f924d1ca899cbc0c", + "sigma_2": "5dd288d659e0a2dd3ef7523a9cc4f80f4a7f919e9980005c7fbec0961d3fb500", + "sigma_3": "3e62e7461db9ca1ed2f1549a8114bbc87fa9242ce0012ed3f9ac9dcf23f4c30a", + "sigma_4": "684c44e7aba416a1982a8db8ec2a3095f5cc6a3f958a4716b69ae76524dd7200", + "sigma_sum": "f0bc5d356344d51f816ea8fa076fa029f7590120136bec7c6958b9081f7864d5" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.json b/frost-secp256k1-tr/tests/helpers/samples.json new file mode 100644 index 000000000..1cc174a9e --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.json @@ -0,0 +1,7 @@ +{ + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.rs b/frost-secp256k1-tr/tests/helpers/samples.rs new file mode 100644 index 000000000..8c39a9e61 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.rs @@ -0,0 +1,179 @@ +//! Generate sample, fixed instances of structs for testing. + +use std::collections::BTreeMap; + +use frost_core::{round1::Nonce, Ciphersuite, Element, Group, Scalar}; +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, SigningShare, VerifiableSecretSharingCommitment, + VerifyingShare, + }, + round1::{NonceCommitment, SigningCommitments, SigningNonces}, + round2::SignatureShare, + Field, Signature, SigningPackage, VerifyingKey, +}; + +type C = frost_secp256k1_tr::Secp256K1Sha256TR; + +fn element1() -> Element { + ::Group::generator() +} + +fn element2() -> Element { + element1() + element1() +} + +fn scalar1() -> Scalar { + let one = <::Group as Group>::Field::one(); + let three = one + one + one; + // To return a fixed non-small number, get the inverse of 3 + <::Group as Group>::Field::invert(&three) + .expect("nonzero elements have inverses") +} + +/// Generate a sample SigningCommitments. +pub fn signing_nonces() -> SigningNonces { + let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); + + SigningNonces::from_nonces(hiding_nonce, binding_nonce) +} + +/// Generate a sample SigningCommitments. +pub fn signing_commitments() -> SigningCommitments { + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); + + SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) +} + +/// Generate a sample SigningPackage. +pub fn signing_package() -> SigningPackage { + let identifier = 42u16.try_into().unwrap(); + let commitments = BTreeMap::from([(identifier, signing_commitments())]); + let message = "hello world".as_bytes(); + + SigningPackage::new(commitments, message) +} + +/// Generate a sample SignatureShare. +pub fn signature_share() -> SignatureShare { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() +} + +/// Generate a sample SecretShare. +pub fn secret_share() -> SecretShare { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + SecretShare::new(identifier, signing_share, vss_commitment) +} + +/// Generate a sample KeyPackage. +pub fn key_package() -> KeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + + KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) +} + +/// Generate a sample PublicKeyPackage. +pub fn public_key_package() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) +} + +/// Generate a sample round1::Package. +pub fn round1_package() -> round1::Package { + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::Package::new(vss_commitment, signature) +} + +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + +/// Generate a sample round2::Package. +pub fn round2_package() -> round2::Package { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + + round2::Package::new(signing_share) +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json new file mode 100644 index 000000000..2fedbec24 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors.json b/frost-secp256k1-tr/tests/helpers/vectors.json new file mode 100644 index 000000000..2fedbec24 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors_dkg.json b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json new file mode 100644 index 000000000..55755fbad --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json @@ -0,0 +1,51 @@ +{ + "config": { + "MAX_PARTICIPANTS": 3, + "MIN_PARTICIPANTS": 2, + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "verifying_key": "03849089de77b56bd35fcbfc70bf38e73448131090acc75d538a5cea63cc3dcefe", + "1": { + "identifier": 1, + "signing_key": "68e3f6904c6043973515a36bf7801a71597da35733f21305d75a5234f06e4529", + "coefficient": "25d2d840a3e2718a431ec69e14ee8a015b000d43c7a9868060f01d5aa52a19d1", + "vss_commitments": ["03e7ba4acb164d2bd5eba4f47b3a788109ddb3f88f1181792424fa332123a25ea8", "037495e920a1f032916193aa80ea97a4c3a611dec9ab47ccc969deb664f5f88bbe"], + "proof_of_knowledge": "191a4ef1851286e2fd6cebd483385452cbb12f43386241854939252c4ed8846b8631f9e69be37ccbd3a0b4593a8f63738747e165a22d0b5786eeb74e59a17837", + "signing_shares": { + "2": "1dd3cb3e2370e6af22917415f0ad584514807b58b3cc40d2230a26e115f02771", + "3": "dd25ee86acd01f996618aa0d1153f5e8fbc929a8e8a18b8f0a15f91d087217e2" + }, + "verifying_share": "02a8bf413b5d7af0e692fba967540cde8009f161a4d721f8c88649c1933bbb7531", + "signing_share": "f1be455a8ec9ab86ef8438f23a5cfdf70153aa2785d4bebba83e0840403e4bf3" + }, + "2": { + "identifier": 2, + "signing_key": "2619be8223b23e0453ddc630a4d164e81f7d8a9e07af33c4d4d02190df8bec13", + "coefficient": "f7ba0cbbffbea8aaceb3ade54bdbf35bafb1cda15b65ad490e0c63dd069a7c9f", + "vss_commitments": ["03ef10370a008cd95e179dc51e2cb7828f30b72d254e5166484f927c84ab326582", "022ce0dac0db217ba326fbbe3e6132d45e2a4bfa0a0c3790d91eacce9a1c2d6a10"], + "proof_of_knowledge": "a319dd51cf64b3896c22f54154812d4ae76cfa95f46f53ef69241fd702456fef32da76cc93d3a541ca495b723e793ee90c32440da5f314e2e58a2dc30550314a", + "signing_shares": { + "1": "b489a711942526abbb5330a8215d2e740f7dbddec3452006993a8cea3ac278cb", + "3": "20255dc07b1fb78bdf90bd85fd2389c988c8250faee11826656a09142fa9fc97" + }, + "verifying_share": "029ecb3a4db28a82e7b8d600d42711b02790dde3f063f0ecec6f812c1c5d7dcefc", + "signing_share": "9131f1241cd8f95f6439b0f5edc2ecb969a2d3db9c85fe5added77116d41d0ce" + }, + "3": { + "identifier": 3, + "signing_key": "9a267f4cde8087a6eca0969425846209b41b515b73195ebbeeef8a991103f1ec", + "coefficient": "42ff6f39ce4f97f279781378ebcf93df47add84d75882cd31b266e83f76e25f6", + "vss_commitments": ["02da186c3863c5600b471a2799cb6f15ae4d8315a2f225c177798880e75ac820a0", "03e6a36e7fa4b117c1aa428886672e3a35d926bb4c585a9b07d8ee9a3387420067"], + "proof_of_knowledge": "6e115d9e63fd15d432b380ccf1ec4ed03340fcf96caeae8985aedb5f905b1a65dc422ffe5878988fbbc55454857736c7755d9c8f5ee6822c8833ea21d54dba36", + "signing_shares": { + "1": "da5c7f5238079835fe71f746364bb8756a7dcb228aeea686fa2aaa44dfec929c", + "2": "0d47e4b622ee3804bff8cfe088653efefe865cce0c065aecbf7e318182b89e2d" + }, + "verifying_share": "02c98b3c2e9f4bde4cf90dc9c7be639e5adda6ea09fc605239880a22cb836f7145", + "signing_share": "30a59cedaae84737d8ef28f9a128db7bd1f1fd8fb3373dfa139ce5e29a4555a9" + } + } +} diff --git a/frost-secp256k1-tr/tests/integration_tests.rs b/frost-secp256k1-tr/tests/integration_tests.rs new file mode 100644 index 000000000..682db377d --- /dev/null +++ b/frost-secp256k1-tr/tests/integration_tests.rs @@ -0,0 +1,312 @@ +use frost_secp256k1_tr::*; +use lazy_static::lazy_static; +use serde_json::Value; + +#[test] +fn check_zero_key_fails() { + frost_core::tests::ciphersuite_generic::check_zero_key_fails::(); +} + +#[test] +fn check_sign_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); +} + +#[test] +fn check_dkg_part1_fails_with_invalid_signers_min_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_min_signers_greater_than_max() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_invalid_signers_max_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_rts() { + let rng = rand::rngs::OsRng; + + frost_core::tests::repairable::check_rts::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_invalid_max_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +/// This is testing that Shamir's secret sharing to compute and arbitrary +/// value is working. +#[test] +fn check_share_generation_secp256k1_tr_sha256() { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::check_share_generation::(rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 0; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_min_signers_greater_than_max() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_max_signers() { + let rng = rand::rngs::OsRng; + + let min_signers = 3; + let max_signers = 0; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +lazy_static! { + pub static ref VECTORS: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_BIG_IDENTIFIER: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors-big-identifier.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_DKG: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors_dkg.json").trim()) + .expect("Test vector is valid JSON"); +} + +#[test] +fn check_sign_with_test_vectors() { + frost_core::tests::vectors::check_sign_with_test_vectors::(&VECTORS); +} + +#[test] +fn check_sign_with_test_vectors_dkg() { + frost_core::tests::vectors_dkg::check_dkg_keygen::(&VECTORS_DKG); +} + +#[test] +fn check_sign_with_test_vectors_with_big_identifiers() { + frost_core::tests::vectors::check_sign_with_test_vectors::( + &VECTORS_BIG_IDENTIFIER, + ); +} + +#[test] +fn check_error_culprit() { + frost_core::tests::ciphersuite_generic::check_error_culprit::(); +} + +#[test] +fn check_identifier_derivation() { + frost_core::tests::ciphersuite_generic::check_identifier_derivation::(); +} + +// Explicit test which is used in a documentation snippet +#[test] +#[allow(unused_variables)] +fn check_identifier_generation() -> Result<(), Error> { + // ANCHOR: dkg_identifier + let participant_identifier = Identifier::try_from(7u16)?; + let participant_identifier = Identifier::derive("alice@example.com".as_bytes())?; + // ANCHOR_END: dkg_identifier + Ok(()) +} + +#[test] +fn check_sign_with_dealer_and_identifiers() { + let rng = rand::rngs::OsRng; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_missing_identifier() { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_incorrect_commitments() { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng).await; + }) + .await + .unwrap(); +} diff --git a/frost-secp256k1-tr/tests/interoperability_tests.rs b/frost-secp256k1-tr/tests/interoperability_tests.rs new file mode 100644 index 000000000..3cf68f217 --- /dev/null +++ b/frost-secp256k1-tr/tests/interoperability_tests.rs @@ -0,0 +1,52 @@ +use frost_secp256k1_tr::*; + +use crate::Secp256K1Sha256TR; + +mod helpers; + +#[test] +fn check_interoperability_in_regular_sign() { + let mut rng = rand::rngs::OsRng; + + for _ in 0..256 { + let signing_key = SigningKey::new(&mut rng); + let verifying_key = (&signing_key).into(); + let signature = signing_key.sign(rng, b"message"); + helpers::verify_signature(b"message", &signature, &verifying_key); + } +} + +#[test] +fn check_interoperability_in_sign_with_dkg() { + let rng = rand::rngs::OsRng; + + // Test with multiple keys/signatures to better exercise the key generation + // and the interoperability check. A smaller number of iterations is used + // because DKG takes longer and otherwise the test would be too slow. + for _ in 0..32 { + let (message, group_signature, group_pubkey) = + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::( + rng, + ); + + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} + +#[test] +fn check_interoperability_in_sign_with_dealer() { + let rng = rand::rngs::OsRng; + + // Test with multiple keys/signatures to better exercise the key generation + // and the interoperability check. + for _ in 0..256 { + let (message, group_signature, group_pubkey) = + frost_core::tests::ciphersuite_generic::check_sign_with_dealer::( + rng, + ); + + // Check that the threshold signature can be verified by the `ed25519_dalek` crate + // public key (interoperability test) + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} diff --git a/frost-secp256k1-tr/tests/recreation_tests.rs b/frost-secp256k1-tr/tests/recreation_tests.rs new file mode 100644 index 000000000..56cd8f443 --- /dev/null +++ b/frost-secp256k1-tr/tests/recreation_tests.rs @@ -0,0 +1,194 @@ +//! Test for recreating packages from their components, which shows that they +//! can be serialized and deserialized as the user wishes. + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +mod helpers; + +use helpers::samples; + +/// Check if SigningNonces can be recreated. +#[test] +fn check_signing_nonces_recreation() { + let nonces = samples::signing_nonces(); + let hiding = nonces.hiding(); + let binding = nonces.binding(); + let new_nonces = SigningNonces::from_nonces(*hiding, *binding); + assert!(nonces == new_nonces); +} + +/// Check if SigningCommitments can be recreated. +#[test] +fn check_signing_commitments_recreation() { + let commitments = samples::signing_commitments(); + let hiding = commitments.hiding(); + let binding = commitments.binding(); + let new_commitments = SigningCommitments::new(*hiding, *binding); + assert!(commitments == new_commitments); +} + +/// Check if SigningPackage can be recreated. +#[test] +fn check_signing_package_recreation() { + let signing_package = samples::signing_package(); + + let commitments = signing_package.signing_commitments(); + let message = signing_package.message(); + + let new_signing_package = SigningPackage::new(commitments.clone(), message); + assert!(signing_package == new_signing_package); +} + +/// Check if SignatureShare can be recreated. +#[test] +fn check_signature_share_recreation() { + let signature_share = samples::signature_share(); + + let encoded = signature_share.serialize(); + + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); + assert!(signature_share == new_signature_share); +} + +/// Check if SecretShare can be recreated. +#[test] +fn check_secret_share_recreation() { + let secret_share = samples::secret_share(); + + let identifier = secret_share.identifier(); + let value = secret_share.signing_share(); + let commitment = secret_share.commitment(); + + let new_secret_share = SecretShare::new(*identifier, *value, commitment.clone()); + + assert!(secret_share == new_secret_share); +} + +/// Check if KeyPackage can be recreated. +#[test] +fn check_key_package_recreation() { + let key_package = samples::key_package(); + + let identifier = key_package.identifier(); + let signing_share = key_package.signing_share(); + let verifying_share = key_package.verifying_share(); + let verifying_key = key_package.verifying_key(); + let min_signers = key_package.min_signers(); + + let new_key_package = KeyPackage::new( + *identifier, + *signing_share, + *verifying_share, + *verifying_key, + *min_signers, + ); + + assert!(key_package == new_key_package); +} + +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_recreation() { + let public_key_package = samples::public_key_package(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + +/// Check if round1::Package can be recreated. +#[test] +fn check_round1_package_recreation() { + let round1_package = samples::round1_package(); + + let vss_commitment = round1_package.commitment(); + let signature = round1_package.proof_of_knowledge(); + + let new_round1_package = round1::Package::new(vss_commitment.clone(), *signature); + + assert!(round1_package == new_round1_package); +} + +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + +/// Check if round2::Package can be recreated. +#[test] +fn check_round2_package_recreation() { + let round2_package = samples::round2_package(); + + let signing_share = round2_package.signing_share(); + + let new_round2_package = round2::Package::new(*signing_share); + + assert!(round2_package == new_round2_package); +} diff --git a/frost-secp256k1-tr/tests/rerandomized_tests.rs b/frost-secp256k1-tr/tests/rerandomized_tests.rs new file mode 100644 index 000000000..7b4144c9b --- /dev/null +++ b/frost-secp256k1-tr/tests/rerandomized_tests.rs @@ -0,0 +1,9 @@ +use frost_secp256k1_tr::Secp256K1Sha256TR; + +#[test] +fn check_randomized_sign_with_dealer() { + let rng = rand::rngs::OsRng; + + let (_msg, _group_signature, _group_pubkey) = + frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); +} diff --git a/frost-secp256k1-tr/tests/serde_tests.rs b/frost-secp256k1-tr/tests/serde_tests.rs new file mode 100644 index 000000000..e13cf1d9a --- /dev/null +++ b/frost-secp256k1-tr/tests/serde_tests.rs @@ -0,0 +1,648 @@ +#![cfg(feature = "serde")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::SigningCommitments, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; + +#[test] +fn check_signing_commitments_serialization() { + let commitments = samples::signing_commitments(); + + let json = serde_json::to_string_pretty(&commitments).unwrap(); + println!("{}", json); + + let decoded_commitments: SigningCommitments = serde_json::from_str(&json).unwrap(); + assert!(commitments == decoded_commitments); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + let decoded_commitments: SigningCommitments = serde_json::from_str(json).unwrap(); + assert!(commitments == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Wrong ciphersuite + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Wrong, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0000000000000000000000000000000000000000000000000000000000000000", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Ed25519, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signing_package_serialization() { + let signing_package = samples::signing_package(); + + let json = serde_json::to_string_pretty(&signing_package).unwrap(); + println!("{}", json); + + let decoded_signing_package: SigningPackage = serde_json::from_str(&json).unwrap(); + assert!(signing_package == decoded_signing_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + let decoded_signing_package: SigningPackage = serde_json::from_str(json).unwrap(); + assert!(signing_package == decoded_signing_package); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "0000000000000000000000000000000000000000000000000000000000000000": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64", + "extra": 1 + } + "#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signature_share_serialization() { + let signature_share = samples::signature_share(); + + let json = serde_json::to_string_pretty(&signature_share).unwrap(); + println!("{}", json); + + let decoded_signature_share: SignatureShare = serde_json::from_str(&json).unwrap(); + assert!(signature_share == decoded_signature_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_commitments: SignatureShare = serde_json::from_str(json).unwrap(); + assert!(signature_share == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_secret_share_serialization() { + let secret_share = samples::secret_share(); + + let json = serde_json::to_string_pretty(&secret_share).unwrap(); + println!("{}", json); + + let decoded_secret_share: SecretShare = serde_json::from_str(&json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + let decoded_secret_share: SecretShare = serde_json::from_str(json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + "extra": 1, + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_key_package_serialization() { + let key_package = samples::key_package(); + + let json = serde_json::to_string_pretty(&key_package).unwrap(); + println!("{}", json); + + let decoded_key_package: KeyPackage = serde_json::from_str(&json).unwrap(); + assert!(key_package == decoded_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + let decoded_key_package: KeyPackage = serde_json::from_str(json).unwrap(); + assert!(key_package == decoded_key_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra_field": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid version + let invalid_json = r#"{ + "header": { + "version": 1, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "secret_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "group_public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_public_key_package_serialization() { + let public_key_package = samples::public_key_package_new(); + + let json = serde_json::to_string_pretty(&public_key_package).unwrap(); + println!("{}", json); + + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(&json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "0000000000000000000000000000000000000000000000000000000000000000": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round1_package_serialization() { + let round1_package = samples::round1_package(); + + let json = serde_json::to_string_pretty(&round1_package).unwrap(); + println!("{}", json); + + let decoded_round1_package: round1::Package = serde_json::from_str(&json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round1_package: round1::Package = serde_json::from_str(json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "foo": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round2_package_serialization() { + let round2_package = samples::round2_package(); + + let json = serde_json::to_string_pretty(&round2_package).unwrap(); + println!("{}", json); + + let decoded_round2_package: round2::Package = serde_json::from_str(&json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round2_package: round2::Package = serde_json::from_str(json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} diff --git a/frost-secp256k1-tr/tests/serialization_tests.rs b/frost-secp256k1-tr/tests/serialization_tests.rs new file mode 100644 index 000000000..997501730 --- /dev/null +++ b/frost-secp256k1-tr/tests/serialization_tests.rs @@ -0,0 +1,138 @@ +#![cfg(feature = "serialization")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; +use insta::assert_snapshot; + +#[test] +fn check_signing_nonces_postcard_serialization() { + let nonces = samples::signing_nonces(); + let bytes: Vec<_> = nonces.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(nonces, SigningNonces::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_signing_commitments_postcard_serialization() { + let commitments = samples::signing_commitments(); + let bytes: Vec<_> = commitments.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + commitments, + SigningCommitments::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signing_package_postcard_serialization() { + let signing_package = samples::signing_package(); + let bytes: Vec<_> = signing_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signing_package, + SigningPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signature_share_postcard_serialization() { + let signature_share = samples::signature_share(); + let bytes = signature_share.serialize(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); +} +#[test] +fn check_secret_share_postcard_serialization() { + let secret_share = samples::secret_share(); + let bytes: Vec<_> = secret_share.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(secret_share, SecretShare::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_key_package_postcard_serialization() { + let key_package = samples::key_package(); + let bytes: Vec<_> = key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(key_package, KeyPackage::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_public_key_package_postcard_serialization() { + let public_key_package = samples::public_key_package(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_package_postcard_serialization() { + let round1_package = samples::round1_package(); + let bytes: Vec<_> = round1_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_package, + round1::Package::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round2_package_postcard_serialization() { + let round2_package = samples::round2_package(); + let bytes: Vec<_> = round2_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_package, + round2::Package::deserialize(&bytes).unwrap() + ); +} diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap new file mode 100644 index 000000000..ca169f4c9 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b810279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..b147c34cf --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980102 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap new file mode 100644 index 000000000..5600403fa --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap new file mode 100644 index 000000000..9099b14ed --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..0185b1dad --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980203 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap new file mode 100644 index 000000000..218294fb3 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..e8a499199 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b810203 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap new file mode 100644 index 000000000..82e3585a9 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap new file mode 100644 index 000000000..aa7a50309 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(bytes)" +--- +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap new file mode 100644 index 000000000..66962d3c1 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap new file mode 100644 index 000000000..537b8e388 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b8100230f8ab3034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap new file mode 100644 index 000000000..b398e5e13 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee50b68656c6c6f20776f726c64 diff --git a/frost-secp256k1-tr/tests/tweaking_tests.rs b/frost-secp256k1-tr/tests/tweaking_tests.rs new file mode 100644 index 000000000..dddbed12b --- /dev/null +++ b/frost-secp256k1-tr/tests/tweaking_tests.rs @@ -0,0 +1,149 @@ +use std::{error::Error, vec}; + +use k256::elliptic_curve::point::AffineCoordinates; +use k256::ProjectivePoint; +use keys::Tweak; +use sha2::{Digest, Sha256}; + +use frost_secp256k1_tr::*; + +mod helpers; + +#[test] +fn check_tweaked_sign_with_dealer() -> Result<(), Box> { + use frost_secp256k1_tr as frost; + + use std::collections::BTreeMap; + + let merkle_root: Vec = vec![12; 32]; + + let mut rng = rand::rngs::OsRng; + let max_signers = 5; + let min_signers = 3; + let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + rng, + )?; + let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + for (identifier, secret_share) in shares { + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + key_packages.insert(identifier, key_package); + } + + let mut nonces_map = BTreeMap::new(); + let mut commitments_map = BTreeMap::new(); + + for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), &mut rng); + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); + } + + let mut signature_shares = BTreeMap::new(); + let message = "message to sign".as_bytes(); + let signing_package = frost::SigningPackage::new(commitments_map, message); + + for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; + let signature_share = frost::round2::sign_with_tweak( + &signing_package, + nonces, + key_package, + Some(&merkle_root), + )?; + signature_shares.insert(*participant_identifier, signature_share); + } + + let group_signature = frost::aggregate_with_tweak( + &signing_package, + &signature_shares, + &pubkey_package, + Some(&merkle_root), + )?; + + pubkey_package + .verifying_key() + .verify(message, &group_signature) + .expect_err("signature should not be valid for untweaked pubkey_package"); + + let pubkey_package_tweaked = pubkey_package.clone().tweak(Some(&merkle_root)); + pubkey_package_tweaked + .verifying_key() + .verify(message, &group_signature) + .expect("signature should be valid for tweaked pubkey_package"); + + helpers::verify_signature( + message, + &group_signature, + pubkey_package_tweaked.verifying_key(), + ); + + // Confirm the internal (untweaked) group key can be provided to access + // script spending paths under the output (tweaked) group key. + let (expected_parity, expected_tr_output_pubkey) = taproot_tweak_pubkey( + pubkey_package + .verifying_key() + .to_element() + .to_affine() + .x() + .into(), + &merkle_root, + ); + + let tr_output_point = pubkey_package_tweaked + .verifying_key() + .to_element() + .to_affine(); + + let tr_output_pubkey: [u8; 32] = tr_output_point.x().into(); + let tr_output_parity: bool = tr_output_point.y_is_odd().into(); + + assert_eq!( + tr_output_pubkey, expected_tr_output_pubkey, + "taproot output pubkey does not match" + ); + + assert_eq!( + tr_output_parity, expected_parity, + "taproot output pubkey parity bit does not match" + ); + + Ok(()) +} + +/// Emulates the BIP341 helper function: +/// +/// def taproot_tweak_pubkey(pubkey, h): +/// t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)) +/// if t >= SECP256K1_ORDER: +/// raise ValueError +/// P = lift_x(int_from_bytes(pubkey)) +/// if P is None: +/// raise ValueError +/// Q = point_add(P, point_mul(G, t)) +/// return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q)) +/// +fn taproot_tweak_pubkey(pubkey: [u8; 32], merkle_root: &[u8]) -> (bool, [u8; 32]) { + let prefix = Sha256::digest(b"TapTweak"); + let tweak_hash = Sha256::new() + .chain_update(prefix) + .chain_update(prefix) + .chain_update(pubkey) + .chain_update(merkle_root) + .finalize(); + let t = k256::Scalar::from( + k256::elliptic_curve::ScalarPrimitive::new(k256::U256::from_be_slice(&tweak_hash)).unwrap(), + ); + + let mut pubkey_even_bytes = [0x02; 33]; + pubkey_even_bytes[1..].copy_from_slice(&pubkey); + let pubkey_even = Secp256K1Group::deserialize(&pubkey_even_bytes).unwrap(); + + let tr_output_key = (pubkey_even + ProjectivePoint::GENERATOR * t).to_affine(); + (tr_output_key.y_is_odd().into(), tr_output_key.x().into()) +} diff --git a/frost-secp256k1/CHANGELOG.md b/frost-secp256k1/CHANGELOG.md new file mode 100644 index 000000000..b1dedcb2b --- /dev/null +++ b/frost-secp256k1/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Refer to the [`frost-core` +changelog](https://github.com/ZcashFoundation/frost/blob/main/frost-core/CHANGELOG.md). \ No newline at end of file diff --git a/frost-secp256k1/Cargo.toml b/frost-secp256k1/Cargo.toml index 30e726a38..8d942718e 100644 --- a/frost-secp256k1/Cargo.toml +++ b/frost-secp256k1/Cargo.toml @@ -1,20 +1,14 @@ [package] name = "frost-secp256k1" -edition = "2021" -# When releasing to crates.io: -# - Update CHANGELOG.md -# - Create git tag. -version = "1.0.0" -authors = [ - "Deirdre Connolly ", - "Chelsea Komlo ", - "Conrado Gouvea " -] +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true readme = "README.md" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ZcashFoundation/frost" -categories = ["cryptography"] -keywords = ["cryptography", "crypto", "threshold", "signature"] +license.workspace = true +repository.workspace = true +categories.workspace = true +keywords = ["cryptography", "crypto", "secp256k1", "threshold", "signature"] description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST." [package.metadata.docs.rs] @@ -22,36 +16,35 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } -k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"] } -rand_core = "0.6" -sha2 = "0.10.2" +document-features.workspace = true +frost-core.workspace = true +frost-rerandomized.workspace = true +k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"], default-features = false } +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] -criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } -insta = { version = "1.31.0", features = ["yaml"] } -hex = "0.4.3" -lazy_static = "1.4" -proptest = "1.0" -rand = "0.8" -rand_chacha = "0.3" -serde_json = "1.0" +criterion.workspace = true +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } +insta.workspace = true +hex.workspace = true +lazy_static.workspace = true +proptest.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection"] -serialization = ["serde", "frost-core/serialization"] +default = ["serialization"] #! ## Features ## Enable `serde` support for types that need to be communicated. You ## can use `serde` to serialize structs with any encoder that supports ## `serde` (e.g. JSON with `serde_json`). serde = ["frost-core/serde"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-secp256k1/README.md b/frost-secp256k1/README.md index 0d420737a..c65e5789d 100644 --- a/frost-secp256k1/README.md +++ b/frost-secp256k1/README.md @@ -1,6 +1,13 @@ An implementation of Schnorr signatures on the secp256k1 curve for both single and threshold numbers of signers (FROST). +This crate is a re-export of the ciphersuite-generic +[frost-core](https://crates.io/crates/frost-core) crate, parametrized with the +secp256k1 curve. For more details, refer to [The ZF FROST +Book](https://frost.zfnd.org/). + +*This crate is not compatible with Bitcoin BIP-340 (Taproot) signatures. Use [frost-secp256k1-tr](https://crates.io/crates/frost-secp256k1-tr) instead* + ## Example: key generation with trusted dealer and FROST signing Creating a key with a trusted dealer and splitting into shares; then signing a message @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_secp256k1 as frost; -use rand::thread_rng; use std::collections::BTreeMap; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; let (shares, pubkey_package) = frost::keys::generate_with_dealer( @@ -45,14 +51,14 @@ let mut commitments_map = BTreeMap::new(); //////////////////////////////////////////////////////////////////////////// // In practice, each iteration of this loop will be executed by its respective participant. -for participant_index in 1..(min_signers as u16 + 1) { +for participant_index in 1..=min_signers { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _threshold_. # // ANCHOR: round1_commit let (nonces, commitments) = frost::round1::commit( - key_packages[&participant_identifier].signing_share(), + key_package.signing_share(), &mut rng, ); # // ANCHOR_END: round1_commit diff --git a/frost-secp256k1/benches/bench.rs b/frost-secp256k1/benches/bench.rs index c5773633d..cd89e8e2d 100644 --- a/frost-secp256k1/benches/bench.rs +++ b/frost-secp256k1/benches/bench.rs @@ -1,16 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_secp256k1::*; fn bench_secp256k1_batch_verify(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_batch_verify::(c, "secp256k1", &mut rng); } fn bench_secp256k1_sign(c: &mut Criterion) { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; frost_core::benches::bench_sign::(c, "secp256k1", &mut rng); } diff --git a/frost-secp256k1/dkg.md b/frost-secp256k1/dkg.md index ac0980bad..48d76819f 100644 --- a/frost-secp256k1/dkg.md +++ b/frost-secp256k1/dkg.md @@ -3,35 +3,27 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from -a `u16`. The process in which these identifiers are allocated is up to the application. - -The distributed key generation process has 3 parts, with 2 communication rounds -between them, in which each participant needs to send a "package" to every other -participant. In the first round, each participant sends the same package -(a [`round1::Package`]) to every other. In the second round, each receiver gets -their own package (a [`round2::Package`]). - -Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] -that MUST be kept secret. Between part 2 and 3, each participant needs to hold -onto a [`round2::SecretPackage`]. - -After the third part, each participant will get a [`KeyPackage`] with their -long-term secret share that must be kept secret, and a [`PublicKeyPackage`] -that is public (and will be the same between all participants). With those -they can proceed to sign messages with FROST. - +For a higher level tutorial on how to use it, refer to the [ZF FROST +Book](https://frost.zfnd.org/tutorial/dkg.html). ## Example +This example shows the whole procedure in a single program. Of course, in +practice, each participant will run their own part in their own devices and +packages will need to be sent between them, respecting the DKG requirements of +using [authenticated and confidential communication +channels](https://frost.zfnd.org/terminology.html#peer-to-peer-channel), +additionally with a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) for the +first round of communication to ensure all participants have the same value. + ```rust # // ANCHOR: dkg_import -use rand::thread_rng; use std::collections::BTreeMap; use frost_secp256k1 as frost; -let mut rng = thread_rng(); +let mut rng = rand::rngs::OsRng; let max_signers = 5; let min_signers = 3; @@ -48,7 +40,10 @@ let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through a [**broadcast +// channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) +// on top of an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG protocol. @@ -70,7 +65,10 @@ for participant_index in 1..=max_signers { // "Send" the round 1 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through a [**broadcast + // channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + // on top of an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). for receiver_participant_index in 1..=max_signers { if receiver_participant_index == participant_index { continue; @@ -96,7 +94,8 @@ let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given participant. // This is used to simulate the broadcast; in practice the packages -// will be sent through some communication channel. +// will be sent through an [authenticated and confidential communication +// channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG protocol. @@ -118,7 +117,8 @@ for participant_index in 1..=max_signers { // "Send" the round 2 package to all other participants. In this // test this is simulated using a BTreeMap; in practice this will be - // sent through some communication channel. + // sent through an [authenticated and confidential communication + // channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). // Note that, in contrast to the previous part, here each other participant // gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { diff --git a/frost-secp256k1/src/keys/dkg.rs b/frost-secp256k1/src/keys/dkg.rs index 91c6286ce..9ea40b263 100644 --- a/frost-secp256k1/src/keys/dkg.rs +++ b/frost-secp256k1/src/keys/dkg.rs @@ -45,7 +45,7 @@ pub mod round2 { /// /// It returns the [`round1::SecretPackage`] that must be kept in memory /// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. +/// must be sent to each other participant in the DKG run. pub fn part1( identifier: Identifier, max_signers: u16, @@ -55,13 +55,21 @@ pub fn part1( frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) } -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. /// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. pub fn part2( secret_package: round1::SecretPackage, round1_packages: &BTreeMap, @@ -70,14 +78,22 @@ pub fn part2( } /// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. /// /// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. pub fn part3( round2_secret_package: &round2::SecretPackage, round1_packages: &BTreeMap, diff --git a/frost-secp256k1/src/keys/refresh.rs b/frost-secp256k1/src/keys/refresh.rs new file mode 100644 index 000000000..bb4b6dd64 --- /dev/null +++ b/frost-secp256k1/src/keys/refresh.rs @@ -0,0 +1,64 @@ +//! Refresh Shares +//! +//! Refer to [`frost_core::keys::refresh`] for more details. + +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_share`]. +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part1`]. +pub fn refresh_dkg_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::refresh::refresh_dkg_part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_part2`]. +pub fn refresh_dkg_part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::refresh::refresh_dkg_part2(secret_package, round1_packages) +} + +/// Refer to [`frost_core::keys::refresh::refresh_dkg_shares`]. +pub fn refresh_dkg_shares( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, + old_pub_key_package: PublicKeyPackage, + old_key_package: KeyPackage, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::refresh::refresh_dkg_shares( + round2_secret_package, + round1_packages, + round2_packages, + old_pub_key_package, + old_key_package, + ) +} diff --git a/frost-secp256k1/src/keys/repairable.rs b/frost-secp256k1/src/keys/repairable.rs index 01bb964d2..48b1506aa 100644 --- a/frost-secp256k1/src/keys/repairable.rs +++ b/frost-secp256k1/src/keys/repairable.rs @@ -4,61 +4,67 @@ //! The RTS is used to help a signer (participant) repair their lost share. This is achieved //! using a subset of the other signers know here as `helpers`. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; +use crate::keys::{KeyPackage, PublicKeyPackage}; // This is imported separately to make `gencode` work. // (if it were below, the position of the import would vary between ciphersuites // after `cargo fmt`) -use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore}; use crate::{Error, Secp256K1Sha256}; -use super::{SecretShare, VerifiableSecretSharingCommitment}; +/// A delta value which is the output of part 1 of RTS. +pub type Delta = frost::keys::repairable::Delta; -/// Step 1 of RTS. +/// A sigma value which is the output of part 2 of RTS. +pub type Sigma = frost::keys::repairable::Sigma; + +/// Part 1 of RTS. /// -/// Generates the "delta" values from `helper_i` to help `participant` recover their share -/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` -/// is the share of `helper_i`. +/// Generates the "delta" values from the helper with `key_package_i` to send to +/// `helpers` (which includes the helper with `key_package_i`), to help +/// `participant` recover their share. /// /// Returns a BTreeMap mapping which value should be sent to which participant. -pub fn repair_share_step_1( +pub fn repair_share_part1( helpers: &[Identifier], - share_i: &SecretShare, + key_package_i: &KeyPackage, rng: &mut R, participant: Identifier, -) -> Result, Error> { - frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +) -> Result, Error> { + frost::keys::repairable::repair_share_part1(helpers, key_package_i, rng, participant) } -/// Step 2 of RTS. -/// -/// Generates the `sigma` values from all `deltas` received from `helpers` -/// to help `participant` recover their share. -/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// Part 2 of RTS. /// -/// Returns a scalar -pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { - frost::keys::repairable::repair_share_step_2::(deltas_j) +/// Generates the "sigma" value from all `deltas` received from all helpers. +/// The "sigma" value must be sent to the participant repairing their share. +pub fn repair_share_part2(deltas: &[Delta]) -> Sigma { + frost::keys::repairable::repair_share_part2::(deltas) } -/// Step 3 of RTS +/// Part 3 of RTS. /// -/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the -/// `value` which is the `SigningShare`. -pub fn repair_share_step_3( - sigmas: &[Scalar], +/// The participant with the given `identifier` recovers their `KeyPackage` +/// with the "sigma" values received from all helpers and the `PublicKeyPackage` +/// of the group (which can be sent by any of the helpers). +/// +/// Returns an error if the `min_signers` field is not set in the `PublicKeyPackage`. +/// This happens for `PublicKeyPackage`s created before the 3.0.0 release; +/// in that case, the user should set the `min_signers` field manually. +pub fn repair_share_part3( + sigmas: &[Sigma], identifier: Identifier, - commitment: &VerifiableSecretSharingCommitment, -) -> SecretShare { - frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) + public_key_package: &PublicKeyPackage, +) -> Result { + frost::keys::repairable::repair_share_part3(sigmas, identifier, public_key_package) } #[cfg(test)] mod tests { use lazy_static::lazy_static; - use rand::thread_rng; + use serde_json::Value; use crate::Secp256K1Sha256; @@ -70,30 +76,30 @@ mod tests { } #[test] - fn check_repair_share_step_1() { - let rng = thread_rng(); + fn check_repair_share_part1() { + let rng = rand::rngs::OsRng; - frost_core::tests::repairable::check_repair_share_step_1::(rng); + frost_core::tests::repairable::check_repair_share_part1::(rng); } #[test] - fn check_repair_share_step_2() { - frost_core::tests::repairable::check_repair_share_step_2::(&REPAIR_SHARE); + fn check_repair_share_part2() { + frost_core::tests::repairable::check_repair_share_part2::(&REPAIR_SHARE); } #[test] - fn check_repair_share_step_3() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_3::( + fn check_repair_share_part3() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part3::( rng, &REPAIR_SHARE, ); } #[test] - fn check_repair_share_step_1_fails_with_invalid_min_signers() { - let rng = thread_rng(); - frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + fn check_repair_share_part1_fails_with_invalid_min_signers() { + let rng = rand::rngs::OsRng; + frost_core::tests::repairable::check_repair_share_part1_fails_with_invalid_min_signers::< Secp256K1Sha256, _, >(rng); diff --git a/frost-secp256k1/src/lib.rs b/frost-secp256k1/src/lib.rs index 25501bf78..bebcd1f5d 100644 --- a/frost-secp256k1/src/lib.rs +++ b/frost-secp256k1/src/lib.rs @@ -1,11 +1,13 @@ +#![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc = document_features::document_features!()] -use std::collections::BTreeMap; +extern crate alloc; + +use alloc::collections::BTreeMap; use frost_rerandomized::RandomizedCiphersuite; use k256::{ @@ -26,7 +28,9 @@ use frost_core as frost; mod tests; // Re-exports in our public API -pub use frost_core::{serde, Ciphersuite, Field, FieldError, Group, GroupError}; +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; pub use rand_core; /// An error. @@ -112,21 +116,15 @@ impl Group for Secp256K1Group { ProjectivePoint::GENERATOR } - fn serialize(element: &Self::Element) -> Self::Serialization { + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } let mut fixed_serialized = [0; 33]; let serialized_point = element.to_affine().to_encoded_point(true); let serialized = serialized_point.as_bytes(); - // Sanity check; either it takes all bytes or a single byte (identity). - assert!(serialized.len() == fixed_serialized.len() || serialized.len() == 1); - // Copy to the left of the buffer (i.e. pad the identity with zeroes). - // Note that identity elements shouldn't be serialized in FROST, but we - // do this padding so that this function doesn't have to return an error. - // If this encodes the identity, it will fail when deserializing. - { - let (left, _right) = fixed_serialized.split_at_mut(serialized.len()); - left.copy_from_slice(serialized); - } - fixed_serialized + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) } fn deserialize(buf: &Self::Serialization) -> Result { @@ -155,20 +153,20 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { h.update(i); } let mut output = [0u8; 32]; - output.copy_from_slice(h.finalize().as_slice()); + output.copy_from_slice(h.finalize().as_ref()); output } -fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { let mut u = [Secp256K1ScalarField::zero()]; - hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + hash_to_field::, Scalar>(&[msg], domain, &mut u) .expect("should never return error according to error cases described in ExpandMsgXmd"); u[0] } /// Context string from the ciphersuite in the [spec]. /// -/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-1 +/// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-1 const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-v1"; /// An implementation of the FROST(secp256k1, SHA-256) ciphersuite. @@ -186,60 +184,54 @@ impl Ciphersuite for Secp256K1Sha256 { /// H1 for FROST(secp256k1, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.1 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) } /// H2 for FROST(secp256k1, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.2 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "chal").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"chal"], m) } /// H3 for FROST(secp256k1, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.3 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) } /// H4 for FROST(secp256k1, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.4 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.8 fn H4(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) } /// H5 for FROST(secp256k1, SHA-256) /// - /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.5 + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.10 fn H5(m: &[u8]) -> Self::HashOutput { hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) } /// HDKG for FROST(secp256k1, SHA-256) fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "dkg").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) } /// HID for FROST(secp256k1, SHA-256) fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "id").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) } } impl RandomizedCiphersuite for Secp256K1Sha256 { fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "randomizer").as_bytes(), + &[CONTEXT_STRING.as_bytes(), b"randomizer"], m, )) } @@ -253,7 +245,6 @@ pub type Identifier = frost::Identifier; /// FROST(secp256k1, SHA-256) keys, key generation, key shares. pub mod keys { use super::*; - use std::collections::BTreeMap; /// The identifier list to use when generating key shares. pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, S>; @@ -344,6 +335,7 @@ pub mod keys { pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; pub mod dkg; + pub mod refresh; pub mod repairable; } @@ -436,6 +428,25 @@ pub fn aggregate( frost::aggregate(signing_package, signature_shares, pubkeys) } +/// The type of cheater detection to use. +pub type CheaterDetection = frost::CheaterDetection; + +/// Like [`aggregate()`], but allow specifying a specific cheater detection +/// strategy. +pub fn aggregate_custom( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, + cheater_detection: CheaterDetection, +) -> Result { + frost::aggregate_custom( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + ) +} + /// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256). pub type SigningKey = frost_core::SigningKey; diff --git a/frost-secp256k1/src/rerandomized.rs b/frost-secp256k1/src/rerandomized.rs new file mode 100644 index 000000000..7f7fc932c --- /dev/null +++ b/frost-secp256k1/src/rerandomized.rs @@ -0,0 +1,65 @@ +//! FROST implementation supporting re-randomizable keys. + +use alloc::collections::btree_map::BTreeMap; + +/// Re-randomized FROST signing using the given `randomizer_seed`, which should +/// be sent from the Coordinator using a confidential channel. +/// +/// See [`crate::round2::sign`] for documentation on the other parameters. +pub fn sign_with_randomizer_seed( + signing_package: &crate::SigningPackage, + signer_nonces: &crate::round1::SigningNonces, + key_package: &crate::keys::KeyPackage, + randomizer_seed: &[u8], +) -> Result { + frost_rerandomized::sign_with_randomizer_seed::( + signing_package, + signer_nonces, + key_package, + randomizer_seed, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`]. +/// +/// See [`frost_core::aggregate`] for documentation on the other parameters. +pub fn aggregate( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate::( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// Re-randomized FROST signature share aggregation with the given +/// [`RandomizedParams`] using the given cheater detection strategy. +/// +/// See [`frost_core::aggregate_custom`] for documentation on the other parameters. +pub fn aggregate_custom( + signing_package: &crate::SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &crate::keys::PublicKeyPackage, + cheater_detection: crate::CheaterDetection, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate_custom::( + signing_package, + signature_shares, + pubkeys, + cheater_detection, + randomized_params, + ) +} + +/// A randomizer. A random scalar which is used to randomize the key. +pub type Randomizer = frost_rerandomized::Randomizer; + +/// Randomized parameters for a signing instance of randomized FROST. +pub type RandomizedParams = frost_rerandomized::RandomizedParams; diff --git a/frost-secp256k1/src/tests/batch.rs b/frost-secp256k1/src/tests/batch.rs index b87d22a90..d3b1c6800 100644 --- a/frost-secp256k1/src/tests/batch.rs +++ b/frost-secp256k1/src/tests/batch.rs @@ -1,24 +1,22 @@ -use rand::thread_rng; - use crate::*; #[test] fn check_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::batch_verify::(rng); } #[test] fn check_bad_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::bad_batch_verify::(rng); } #[test] fn empty_batch_verify() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::batch::empty_batch_verify::(rng); } diff --git a/frost-secp256k1/src/tests/coefficient_commitment.rs b/frost-secp256k1/src/tests/coefficient_commitment.rs index d1b6c22c7..7be35ead6 100644 --- a/frost-secp256k1/src/tests/coefficient_commitment.rs +++ b/frost-secp256k1/src/tests/coefficient_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,7 +12,7 @@ lazy_static! { #[test] fn check_serialization_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< Secp256K1Sha256, _, @@ -22,7 +21,7 @@ fn check_serialization_of_coefficient_commitment() { #[test] fn check_create_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< Secp256K1Sha256, _, @@ -37,7 +36,7 @@ fn check_create_coefficient_commitment_error() { #[test] fn check_get_value_of_coefficient_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< Secp256K1Sha256, diff --git a/frost-secp256k1/src/tests/deserialize.rs b/frost-secp256k1/src/tests/deserialize.rs index bce820014..bb8d555bd 100644 --- a/frost-secp256k1/src/tests/deserialize.rs +++ b/frost-secp256k1/src/tests/deserialize.rs @@ -4,7 +4,8 @@ use crate::*; fn check_deserialize_non_canonical() { let mut encoded_generator = ::Group::serialize( &::Group::generator(), - ); + ) + .unwrap(); let r = ::Group::deserialize(&encoded_generator); assert!(r.is_ok()); @@ -35,3 +36,12 @@ fn check_deserialize_identity() { let r = ::Group::deserialize(&encoded_identity); assert_eq!(r, Err(GroupError::MalformedElement)); } + +// Test if deserializing the identifier 0 fails. +// https://github.com/ZcashFoundation/frost/issues/793 +#[test] +fn check_zero_identifier_deserialization() { + let arr: [u8; 32] = [0; 32]; + let r = Identifier::deserialize(&arr); + assert_eq!(r, Err(Error::FieldError(FieldError::InvalidZeroScalar))); +} diff --git a/frost-secp256k1/src/tests/vss_commitment.rs b/frost-secp256k1/src/tests/vss_commitment.rs index 1a09195aa..79aa89a65 100644 --- a/frost-secp256k1/src/tests/vss_commitment.rs +++ b/frost-secp256k1/src/tests/vss_commitment.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; use crate::*; @@ -13,26 +12,51 @@ lazy_static! { #[test] fn check_serialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); } +#[test] +fn check_serialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_serialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment::(rng); } +#[test] +fn check_deserialize_whole_vss_commitment() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment::( + rng, + ); +} + #[test] fn check_deserialize_vss_commitment_error() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( rng, &ELEMENTS, ); } +#[test] +fn check_deserialize_whole_vss_commitment_error() { + let rng = rand::rngs::OsRng; + frost_core::tests::vss_commitment::check_deserialize_whole_vss_commitment_error::< + Secp256K1Sha256, + _, + >(rng, &ELEMENTS); +} + #[test] fn check_compute_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::vss_commitment::check_compute_public_key_package::(rng); } diff --git a/frost-secp256k1/tests/common_traits_tests.rs b/frost-secp256k1/tests/common_traits_tests.rs index 6048b4806..e9f788eb1 100644 --- a/frost-secp256k1/tests/common_traits_tests.rs +++ b/frost-secp256k1/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_secp256k1::SigningKey; use helpers::samples; -use rand::thread_rng; #[allow(clippy::unnecessary_literal_unwrap)] fn check_common_traits_for_type(v: T) { @@ -20,7 +19,7 @@ fn check_common_traits_for_type(v: #[test] fn check_signing_key_common_traits() { - let mut rng = thread_rng(); + let mut rng = rand::rngs::OsRng; let signing_key = SigningKey::new(&mut rng); check_common_traits_for_type(signing_key); } diff --git a/frost-secp256k1/tests/helpers/samples.json b/frost-secp256k1/tests/helpers/samples.json index 210c6f27e..54f6e1e1c 100644 --- a/frost-secp256k1/tests/helpers/samples.json +++ b/frost-secp256k1/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" -} \ No newline at end of file +} diff --git a/frost-secp256k1/tests/helpers/samples.rs b/frost-secp256k1/tests/helpers/samples.rs index e8446fe7c..d9cbf5b92 100644 --- a/frost-secp256k1/tests/helpers/samples.rs +++ b/frost-secp256k1/tests/helpers/samples.rs @@ -36,18 +36,20 @@ fn scalar1() -> Scalar { pub fn signing_nonces() -> SigningNonces { let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); - let hiding_nonce = Nonce::deserialize(serialized_scalar1).unwrap(); - let binding_nonce = Nonce::deserialize(serialized_scalar2).unwrap(); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); SigningNonces::from_nonces(hiding_nonce, binding_nonce) } /// Generate a sample SigningCommitments. pub fn signing_commitments() -> SigningCommitments { - let serialized_element1 = ::Group::serialize(&element1()); - let serialized_element2 = ::Group::serialize(&element2()); - let hiding_nonce_commitment = NonceCommitment::deserialize(serialized_element1).unwrap(); - let binding_nonce_commitment = NonceCommitment::deserialize(serialized_element2).unwrap(); + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) } @@ -65,15 +67,15 @@ pub fn signing_package() -> SigningPackage { pub fn signature_share() -> SignatureShare { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - SignatureShare::deserialize(serialized_scalar).unwrap() + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() } /// Generate a sample SecretShare. pub fn secret_share() -> SecretShare { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); @@ -84,11 +86,11 @@ pub fn secret_share() -> SecretShare { pub fn key_package() -> KeyPackage { let identifier = 42u16.try_into().unwrap(); let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) } @@ -96,38 +98,82 @@ pub fn key_package() -> KeyPackage { /// Generate a sample PublicKeyPackage. pub fn public_key_package() -> PublicKeyPackage { let identifier = 42u16.try_into().unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_share = VerifyingShare::deserialize(serialized_element).unwrap(); - let serialized_element = ::Group::serialize(&element1()); - let verifying_key = VerifyingKey::deserialize(serialized_element).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, None) +} + +/// Generate a sample PublicKeyPackage with `min_signers`. +pub fn public_key_package_new() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key, Some(2)) +} + +/// Generate a sample round1::SecretPackage. +pub fn round1_secret_package() -> round1::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let coefficients = vec![scalar1(), scalar1()]; + let min_signers = 2; + let max_signers = 3; + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::SecretPackage::new( + identifier, + coefficients, + commitment, + min_signers, + max_signers, + ) } /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let serialized_element = ::Group::serialize(&element1()); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>() - .try_into() - .unwrap(); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } +/// Generate a sample round1::SecretPackage. +pub fn round2_secret_package() -> round2::SecretPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + let secret_share = scalar1(); + let min_signers = 2; + let max_signers = 3; + + round2::SecretPackage::new( + identifier, + commitment, + secret_share, + min_signers, + max_signers, + ) +} + /// Generate a sample round2::Package. pub fn round2_package() -> round2::Package { let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); - let signing_share = SigningShare::deserialize(serialized_scalar).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); round2::Package::new(signing_share) } diff --git a/frost-secp256k1/tests/integration_tests.rs b/frost-secp256k1/tests/integration_tests.rs index 58ba3e088..343214e1b 100644 --- a/frost-secp256k1/tests/integration_tests.rs +++ b/frost-secp256k1/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_secp256k1::*; use lazy_static::lazy_static; -use rand::thread_rng; use serde_json::Value; #[test] @@ -10,14 +9,14 @@ fn check_zero_key_fails() { #[test] fn check_sign_with_dkg() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); } #[test] fn check_dkg_part1_fails_with_invalid_signers_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -31,7 +30,7 @@ fn check_dkg_part1_fails_with_invalid_signers_min_signers() { #[test] fn check_dkg_part1_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -45,7 +44,7 @@ fn check_dkg_part1_fails_with_min_signers_greater_than_max() { #[test] fn check_dkg_part1_fails_with_invalid_signers_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -59,21 +58,80 @@ fn check_dkg_part1_fails_with_invalid_signers_max_signers() { #[test] fn check_rts() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_refresh_shares_with_dealer() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( + rng, + ); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Secp256K1Sha256, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = rand::rngs::OsRng; + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256, + _, + >(&identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dkg() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg::(rng); +} + +#[test] +fn check_refresh_shares_with_dkg_smaller_threshold() { + let rng = rand::rngs::OsRng; + + frost_core::tests::refresh::check_refresh_shares_with_dkg_smaller_threshold::( + rng, + ); +} + #[test] fn check_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); } #[test] fn check_sign_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 1; let max_signers = 3; @@ -87,7 +145,7 @@ fn check_sign_with_dealer_fails_with_invalid_min_signers() { #[test] fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -101,7 +159,7 @@ fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { #[test] fn check_sign_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 1; @@ -117,13 +175,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_secp256k1_sha256() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_share_generation::(rng); } #[test] fn check_share_generation_fails_with_invalid_min_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 0; let max_signers = 3; @@ -137,7 +195,7 @@ fn check_share_generation_fails_with_invalid_min_signers() { #[test] fn check_share_generation_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 2; @@ -151,7 +209,7 @@ fn check_share_generation_fails_with_min_signers_greater_than_max() { #[test] fn check_share_generation_fails_with_invalid_max_signers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let min_signers = 3; let max_signers = 0; @@ -215,7 +273,7 @@ fn check_identifier_generation() -> Result<(), Error> { #[test] fn check_sign_with_dealer_and_identifiers() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< Secp256K1Sha256, @@ -225,7 +283,7 @@ fn check_sign_with_dealer_and_identifiers() { #[test] fn check_sign_with_missing_identifier() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::( rng, ); @@ -233,9 +291,19 @@ fn check_sign_with_missing_identifier() { #[test] fn check_sign_with_incorrect_commitments() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::< Secp256K1Sha256, _, >(rng); } + +#[tokio::test] +async fn check_async_sign_with_dealer() { + tokio::spawn(async { + let rng = rand::rngs::OsRng; + frost_core::tests::ciphersuite_generic::async_check_sign::(rng).await; + }) + .await + .unwrap(); +} diff --git a/frost-secp256k1/tests/recreation_tests.rs b/frost-secp256k1/tests/recreation_tests.rs index 0806e55f3..1e8eb2049 100644 --- a/frost-secp256k1/tests/recreation_tests.rs +++ b/frost-secp256k1/tests/recreation_tests.rs @@ -54,7 +54,7 @@ fn check_signature_share_recreation() { let encoded = signature_share.serialize(); - let new_signature_share = SignatureShare::deserialize(encoded).unwrap(); + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); assert!(signature_share == new_signature_share); } @@ -101,12 +101,51 @@ fn check_public_key_package_recreation() { let verifying_shares = public_key_package.verifying_shares(); let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); - let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + let new_public_key_package = + PublicKeyPackage::new_internal(verifying_shares.clone(), *verifying_key, min_signers); assert!(public_key_package == new_public_key_package); } +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_new_recreation() { + let public_key_package = samples::public_key_package_new(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + let min_signers = public_key_package.min_signers(); + + let new_public_key_package = + PublicKeyPackage::new(verifying_shares.clone(), *verifying_key, min_signers); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::SecretPackage can be recreated. +#[test] +fn check_round1_secret_package_recreation() { + let round1_secret_package = samples::round1_secret_package(); + + let identifier = round1_secret_package.identifier(); + let coefficients = round1_secret_package.coefficients(); + let commitment = round1_secret_package.commitment(); + let min_signers = round1_secret_package.min_signers(); + let max_signers = round1_secret_package.max_signers(); + + let new_round1_secret_package = round1::SecretPackage::new( + *identifier, + coefficients.clone(), + commitment.clone(), + *min_signers, + *max_signers, + ); + + assert!(round1_secret_package == new_round1_secret_package); +} + /// Check if round1::Package can be recreated. #[test] fn check_round1_package_recreation() { @@ -120,6 +159,28 @@ fn check_round1_package_recreation() { assert!(round1_package == new_round1_package); } +/// Check if round2::SecretPackage can be recreated. +#[test] +fn check_round2_secret_package_recreation() { + let round2_secret_package = samples::round2_secret_package(); + + let identifier = round2_secret_package.identifier(); + let commitment = round2_secret_package.commitment(); + let secret_share = round2_secret_package.secret_share(); + let min_signers = round2_secret_package.min_signers(); + let max_signers = round2_secret_package.max_signers(); + + let new_round2_secret_package = round2::SecretPackage::new( + *identifier, + commitment.clone(), + secret_share, + *min_signers, + *max_signers, + ); + + assert!(round2_secret_package == new_round2_secret_package); +} + /// Check if round2::Package can be recreated. #[test] fn check_round2_package_recreation() { diff --git a/frost-secp256k1/tests/rerandomized_tests.rs b/frost-secp256k1/tests/rerandomized_tests.rs index 65cf0dff5..c7845f170 100644 --- a/frost-secp256k1/tests/rerandomized_tests.rs +++ b/frost-secp256k1/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_secp256k1::Secp256K1Sha256; -use rand::thread_rng; #[test] fn check_randomized_sign_with_dealer() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; let (_msg, _group_signature, _group_pubkey) = frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); diff --git a/frost-secp256k1/tests/serde_tests.rs b/frost-secp256k1/tests/serde_tests.rs index 82a0735d4..5b02fdaeb 100644 --- a/frost-secp256k1/tests/serde_tests.rs +++ b/frost-secp256k1/tests/serde_tests.rs @@ -434,7 +434,7 @@ fn check_key_package_serialization() { #[test] fn check_public_key_package_serialization() { - let public_key_package = samples::public_key_package(); + let public_key_package = samples::public_key_package_new(); let json = serde_json::to_string_pretty(&public_key_package).unwrap(); println!("{}", json); @@ -450,11 +450,27 @@ fn check_public_key_package_serialization() { "verifying_shares": { "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" }, - "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 }"#; let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); assert!(public_key_package == decoded_public_key_package); + // Old version without min_signers + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package.verifying_key() == decoded_public_key_package.verifying_key()); + assert!(public_key_package.verifying_shares() == decoded_public_key_package.verifying_shares()); + let invalid_json = "{}"; assert!(serde_json::from_str::(invalid_json).is_err()); diff --git a/frost-secp256k1/tests/serialization_tests.rs b/frost-secp256k1/tests/serialization_tests.rs index 0a304a491..08e53d3e4 100644 --- a/frost-secp256k1/tests/serialization_tests.rs +++ b/frost-secp256k1/tests/serialization_tests.rs @@ -49,8 +49,11 @@ fn check_signing_package_postcard_serialization() { fn check_signature_share_postcard_serialization() { let signature_share = samples::signature_share(); let bytes = signature_share.serialize(); - assert_snapshot!(hex::encode(bytes)); - assert_eq!(signature_share, SignatureShare::deserialize(bytes).unwrap()); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); } #[test] fn check_secret_share_postcard_serialization() { @@ -79,6 +82,28 @@ fn check_public_key_package_postcard_serialization() { ); } +#[test] +fn check_public_key_package_new_postcard_serialization() { + let public_key_package = samples::public_key_package_new(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_secret_package_postcard_serialization() { + let round1_secret_package = samples::round1_secret_package(); + let bytes: Vec<_> = round1_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_secret_package, + round1::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round1_package_postcard_serialization() { let round1_package = samples::round1_package(); @@ -90,6 +115,17 @@ fn check_round1_package_postcard_serialization() { ); } +#[test] +fn check_round2_secret_package_postcard_serialization() { + let round2_secret_package = samples::round2_secret_package(); + let bytes: Vec<_> = round2_secret_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_secret_package, + round2::SecretPackage::deserialize(&bytes).unwrap() + ); +} + #[test] fn check_round2_package_postcard_serialization() { let round2_package = samples::round2_package(); diff --git a/frost-secp256k1/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap b/frost-secp256k1/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap new file mode 100644 index 000000000..0435fdaa1 --- /dev/null +++ b/frost-secp256k1/tests/snapshots/serialization_tests__check_public_key_package_new_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00eed6b1b101000000000000000000000000000000000000000000000000000000000000002a0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980102 diff --git a/frost-secp256k1/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap b/frost-secp256k1/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..bd66d2c99 --- /dev/null +++ b/frost-secp256k1/tests/snapshots/serialization_tests__check_round1_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980203 diff --git a/frost-secp256k1/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap b/frost-secp256k1/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap new file mode 100644 index 000000000..b244188fe --- /dev/null +++ b/frost-secp256k1/tests/snapshots/serialization_tests__check_round2_secret_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +000000000000000000000000000000000000000000000000000000000000002a010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b810203 diff --git a/gencode/Cargo.toml b/gencode/Cargo.toml index c99b7a1bb..44dead9ba 100644 --- a/gencode/Cargo.toml +++ b/gencode/Cargo.toml @@ -1,14 +1,12 @@ [package] name = "gencode" version = "0.1.0" -edition = "2021" +edition.workspace = true publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] regex = "1.6.0" -serde_json = "1.0" +serde_json.workspace = true [[bin]] name = "gencode" diff --git a/gencode/src/main.rs b/gencode/src/main.rs index 0806901ed..9dee2d5da 100644 --- a/gencode/src/main.rs +++ b/gencode/src/main.rs @@ -121,17 +121,14 @@ fn write_docs( // To be able to replace the documentation properly, start from the end, which // will keep the string positions consistent for (old_name, _, old_start, old_end) in old_docs.iter().rev() { - let new_doc = docs - .get(old_name) - .unwrap_or_else(|| { - panic!( - "documentation for {} is not available in base file", - old_name - ) - }) - .1 - .clone(); - + let new_doc = docs.get(old_name).map(|v| v.1.clone()); + let Some(new_doc) = new_doc else { + eprintln!( + "WARNING: documentation for {} is not available in base file. This can mean it's a specific type for the ciphersuite, or that there is a bug in gencode", + old_name + ); + continue; + }; // Replaces ciphersuite-references in documentation let mut new_doc = new_doc.to_string(); for (old_n, new_n) in zip(original_suite_strings.iter(), new_suite_strings.iter()) { @@ -170,6 +167,8 @@ fn copy_and_replace( pub fn rustfmt(source: String) -> String { let mut child = Command::new("rustfmt") + .arg("--edition") + .arg("2021") .stdin(Stdio::piped()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) @@ -184,6 +183,11 @@ pub fn rustfmt(source: String) -> String { }); let output = child.wait_with_output().expect("Failed to read stdout"); + assert!( + output.status.success(), + "rustfmt failed: {}", + String::from_utf8_lossy(&output.stderr) + ); String::from_utf8_lossy(&output.stdout).to_string() } @@ -215,6 +219,7 @@ fn main() -> ExitCode { "ristretto255_sha512", "ristretto255", "", + "", ] .iter() .map(|x| x.to_string()) @@ -227,7 +232,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{original_folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { original_strings.push(samples[key].as_str().unwrap().to_owned()); } let original_strings: Vec<&str> = original_strings.iter().map(|s| s.as_ref()).collect(); @@ -249,6 +260,7 @@ fn main() -> ExitCode { "p256_sha256", "p256", "

", + "", ], ), ( @@ -262,6 +274,7 @@ fn main() -> ExitCode { "ed25519_sha512", "ed25519", "", + "", ], ), ( @@ -275,6 +288,7 @@ fn main() -> ExitCode { "ed448_shake256", "ed448", "", + "", ], ), ( @@ -288,6 +302,21 @@ fn main() -> ExitCode { "secp256k1_sha256", "secp256k1", "", + "*This crate is not compatible with Bitcoin BIP-340 (Taproot) signatures. Use [frost-secp256k1-tr](https://crates.io/crates/frost-secp256k1-tr) instead*", + ], + ), + ( + "frost-secp256k1-tr", + &[ + "Secp256K1Sha256TR", + "secp256k1 curve (Taproot)", + "Secp256K1", + "FROST(secp256k1, SHA-256)", + "FROST-secp256k1-SHA256-TR-v1", + "secp256k1_tr_sha256", + "secp256k1_tr", + "", + "", ], ), ] { @@ -300,7 +329,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { replacement_strings.push(samples[key].as_str().unwrap().to_owned()); } let replacement_strings: Vec<&str> = @@ -319,8 +354,11 @@ fn main() -> ExitCode { // Generate files based on a template with simple search & replace. for filename in [ "README.md", + "CHANGELOG.md", "dkg.md", + "src/rerandomized.rs", "src/keys/dkg.rs", + "src/keys/refresh.rs", "src/keys/repairable.rs", "src/tests/batch.rs", "src/tests/coefficient_commitment.rs", diff --git a/performance.md b/performance.md index 6b21dd433..029632de5 100644 --- a/performance.md +++ b/performance.md @@ -6,7 +6,7 @@ FROST is a threshold Schnorr signature scheme [invented](https://eprint.iacr.org/2020/852) by Chelsea Komlo (researcher at the Zcash Foundation) and Ian Goldberg, and in the process of becoming an [IETF -RFC](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/). Threshold +RFC](https://datatracker.ietf.org/doc/rfc9591/). Threshold signatures allow a private key being split into shares given to multiple participants, allowing a subgroup of them (e.g. 3 out of 5, or whatever threshold specified at key generation) to generate a signature that can be diff --git a/plot.py b/plot.py index 9a3a0e2c0..20680b719 100644 --- a/plot.py +++ b/plot.py @@ -1,6 +1,6 @@ """ -Generate the graphs for the FROST perfomance blog post. +Generate the graphs for the FROST performance blog post. Install cargo-criterion: