diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b21dd53..7c0e7e5 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 78b16a2..55ce037 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -23,16 +23,13 @@ jobs: RUST_BACKTRACE: full steps: - - uses: actions/checkout@v4.2.2 + - 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 @@ -44,4 +41,4 @@ jobs: 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@v5.1.2 + uses: codecov/codecov-action@v6.0.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index aebf2fa..b31762c 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,56 +37,51 @@ 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.2.2 + 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@v2.0.0 with: - mdbook-version: '0.4.18' + mdbook-version: '0.5.2' # TODO: actions-mdbook does not yet have an option to install mdbook-mermaid https://github.com/peaceiris/actions-mdbook/issues/426 - name: Install plugins run: | cargo install mdbook-mermaid - cargo install mdbook-admonish - name: Build FROST book 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 aa5ed27..c263aa9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,27 +9,30 @@ on: - main jobs: - build_default: name: build with default features runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 - - 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.66.1) + name: build with MSRV (1.81) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - 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 @@ -40,7 +43,7 @@ jobs: - 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.66.1 + - 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 @@ -51,7 +54,7 @@ jobs: # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v4.2.2 + # - 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 @@ -71,7 +74,7 @@ jobs: matrix: crate: [ristretto255, ed25519, p256, secp256k1, secp256k1-tr, rerandomized] steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v6.0.2 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -84,29 +87,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 - - 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.2.2 + - 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 @@ -117,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 @@ -134,44 +127,34 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - 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.2.2 + - 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 @@ -180,27 +163,20 @@ jobs: RUSTDOCFLAGS: -D warnings steps: - - uses: actions/checkout@v4.2.2 + - 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.2.2 - - uses: reviewdog/action-actionlint@v1.63.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 a110f6d..2e0a7b6 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -8,9 +8,12 @@ on: jobs: update_release_draft: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read 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 5d3d773..bbb0762 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 491837e..7a64a0f 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -9,7 +9,6 @@ queue_rules: # which are the same as the GitHub main branch protection rules # https://docs.mergify.com/conditions/#about-branch-protection - base=main - allow_inplace_checks: true batch_size: 2 # Wait for a few minutes to embark 2 tickets together in a merge train batch_max_wait_time: "3 minutes" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5ab53de --- /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 0000000..216200a --- /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" +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" +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" +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" +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" +dependencies = [ + "derive-getters", + "document-features", + "frost-core", + "hex", + "rand_core 0.6.4", +] + +[[package]] +name = "frost-ristretto255" +version = "3.0.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" +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" +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 d2615d4..cf479d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,12 @@ members = [ "gencode" ] - [workspace.package] +# If you update the edition, make sure to also update the argument to rustfmt +# in gencode/src/main.rs. edition = "2021" -version = "2.1.0" +rust-version = "1.81" +version = "3.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -27,7 +29,9 @@ repository = "https://github.com/ZcashFoundation/frost" categories = ["cryptography"] [workspace.dependencies] -criterion = "0.5" +# 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"] } @@ -37,6 +41,10 @@ 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", default-features = false } +frost-rerandomized = { path = "frost-rerandomized", version = "3.0.0", default-features = false } [profile.test.package."*"] -opt-level = 3 \ No newline at end of file +opt-level = 3 diff --git a/README.md b/README.md index 019b60d..0294816 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ | 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 ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/). +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 @@ -21,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 @@ -29,22 +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 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; -- Rerandomized FROST (paper under review). -- Refresh Share functionality using a Trusted Dealer. This can be used to refresh the shares of the participants or to remove a participant. +- [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 @@ -74,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 e95c664..e988329 100644 --- a/book/book.toml +++ b/book/book.toml @@ -1,17 +1,9 @@ [book] authors = ["Zcash Foundation "] language = "en" -multilingual = false src = "src" title = "The ZF FROST Book" -[preprocessor] - -[preprocessor.admonish] -command = "mdbook-admonish" -assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install` - [output] [output.html] -additional-css = ["./mdbook-admonish.css", "book/mdbook-admonish.css"] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 3970f8e..4ffa00e 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -9,10 +9,13 @@ - [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 27ffb58..7c31eec 100644 --- a/book/src/dev/developer-guide.md +++ b/book/src/dev/developer-guide.md @@ -11,4 +11,4 @@ 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 cc8c13c..98f8853 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 diff --git a/book/src/dev/release-checklist.md b/book/src/dev/release-checklist.md index 7173eb8..5005edf 100644 --- a/book/src/dev/release-checklist.md +++ b/book/src/dev/release-checklist.md @@ -1,110 +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) - 8. [Frost secp256k1 tr version number](https://github.com/ZcashFoundation/frost/blob/main/frost-secp256k1-tr/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 the crates in the root Cargo.toml file. (If they ever - get out of sync, you wil need to bump in each crate Cargo.toml file.) +- 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 secp256k1-tr 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 secp256k1-tr 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 secp256k1 tr](https://crates.io/crates/frost-secp256k1-tr/versions) - 8. [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 7c34073..8dfc4d3 100644 --- a/book/src/frost.md +++ b/book/src/frost.md @@ -10,10 +10,9 @@ together generate a signature that can be validated by the corresponding verifyi key. One important aspect is that the resulting signature is indistinguishable from a non-threshold signature from the point of view of signature verifiers. -```admonish note -FROST only supports Schnorr signatures. Therefore it can't produce -ECDSA signatures. -``` +> [!NOTE] +> FROST only supports Schnorr signatures. Therefore it can't produce +> ECDSA signatures. ## Key Generation @@ -62,21 +61,19 @@ consolidates them and sends them to each participant. Each one will then produce a signature share, which is sent to the Coordinator who finally aggregates them and produces the final signature. -```admonish note -If having a single coordinator is not desired, then all participants -can act as coordinators. Refer to the -[spec](https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#removing-the-coordinator-role-no-coordinator) -for more information. -``` +> [!NOTE] +> If having a single coordinator is not desired, then all participants +> can act as coordinators. Refer to the +> [spec](https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#removing-the-coordinator-role-no-coordinator) +> for more information. -```admonish warning -ALL participants who are selected for generating the signature need -to produce their share, even if there are more than `t` of them. -For example, in 2-of-3 signing, if 3 participants are selected, -them all 3 must produce signature shares in order for the Coordinator -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. -``` +> [!WARNING] +> ALL participants who are selected for generating the signature need +> to produce their share, even if there are more than `t` of them. +> For example, in 2-of-3 signing, if 3 participants are selected, +> them all 3 must produce signature shares in order for the Coordinator +> 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 Signatures @@ -112,20 +109,19 @@ shares in a way that maintains the same group public key. Some applications are: 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. -``` +> [!CAUTION] +> 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 @@ -133,3 +129,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 66fad0a..39ee232 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 1b93053..686dfd0 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 15df64c..5d86ba5 100644 --- a/book/src/tutorial.md +++ b/book/src/tutorial.md @@ -15,3 +15,8 @@ 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. +> [!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 5bab3dd..755dc7d 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 @@ -28,28 +32,31 @@ the application.) It returns a `round1::SecretPackage` and a `round1::Package`: {{#include ../../../frost-ristretto255/dkg.md:dkg_part1}} ``` -```admonish info -Check the crate documentation for a [full working example](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/index.html#example); keep in mind it's an artificial -one since everything runs in the same program. -``` +> [!TIP] +> Check the crate documentation for a [full working example](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/index.html#example); keep in mind it's an artificial +> one since everything runs in the same program. The `round1::SecretPackage` must be kept in memory to use in the next round. The `round1::Package` must be sent to all other participants using a [**broadcast channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) to ensure that all participants receive the same value. -```admonish danger -A [**broadcast -channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) in this -context is not simply broadcasting the value to all participants. It requires -running a protocol to ensure that all participants have the same value or that -the protocol is aborted. Check the linked [Terminology -section](https://frost.zfnd.org/terminology.html#broadcast-channel) for more -details. - -**Failure in using a proper broadcast channel will make the key generation -insecure.** -``` +> [!CAUTION] +> A [**broadcast +> channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) in this +> context is not simply broadcasting the value to all participants. It requires +> running a protocol to ensure that all participants have the same value or that +> 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.** ## Part 2 @@ -75,6 +82,11 @@ 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). +> [!CAUTION] +> 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 @@ -90,6 +102,5 @@ a `PublicKeyPackage` containing the group verifying key: {{#include ../../../frost-ristretto255/dkg.md:dkg_part3}} ``` -```admonish note -All participants will generate the same `PublicKeyPackage`. -``` \ No newline at end of file +> [!NOTE] +> All participants will generate the same `PublicKeyPackage`. \ No newline at end of file diff --git a/book/src/tutorial/importing.md b/book/src/tutorial/importing.md index 2c2dd41..40bc51f 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 = "2.1.0" +frost-ristretto255 = "3.0.0" ``` ## Handling errors @@ -38,7 +38,7 @@ needs to be transmitted. The importing would look like: ``` [dependencies] -frost-ristretto255 = { version = "2.1.0", features = ["serde"] } +frost-ristretto255 = { version = "3.0.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 index fa45ce9..8b6f7a1 100644 --- a/book/src/tutorial/refreshing-shares.md +++ b/book/src/tutorial/refreshing-shares.md @@ -17,24 +17,21 @@ 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 +> [!CAUTION] +> 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. + +> [!CAUTION] +> 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. + +> [!CAUTION] +> 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/signing.md b/book/src/tutorial/signing.md index e978fa4..fea4903 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,12 @@ 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. + +> [!NOTE] +> 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,19 +42,17 @@ 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.) - -```admonish warning -In all of the main FROST ciphersuites, the entire message must -be sent to participants. In some cases, where the message is too big, it may be -necessary to send a hash of the message instead. We strongly suggest creating a -specific ciphersuite for this, and not just sending the hash as if it were the -message. For reference, see [how RFC 8032 handles -"pre-hashing"](https://datatracker.ietf.org/doc/html/rfc8032). -``` +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`.) + +> [!WARNING] +> In all of the main FROST ciphersuites, the entire message must +> be sent to participants. In some cases, where the message is too big, it may be +> necessary to send a hash of the message instead. We strongly suggest creating a +> specific ciphersuite for this, and not just sending the hash as if it were the +> message. For reference, see [how RFC 8032 handles +> "pre-hashing"](https://datatracker.ietf.org/doc/html/rfc8032). ## Participants, Round 2 @@ -63,16 +65,13 @@ 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 -they are signing. Thus the application should show the message to the -participant and obtain their consent to proceed before producing the signature -share. -``` +> [!IMPORTANT] +> In most applications, it is important that the participant must be aware of what +> they are signing. Thus the application should show the message to the +> participant and obtain their consent to proceed before producing the signature +> share. ## Coordinator, Aggregate @@ -90,15 +89,27 @@ with the same `SigningPackage` sent to the participants and the The returned signature, a `Signature`, will be a valid signature for the message in the `SigningPackage` in Round 2 for the group verifying key in the `PublicKeyPackage`. -```admonish note -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.) - -What should be done in that case is up to the application. The misbehaving participant -could be excluded from future signing sessions, for example. -``` +> [!NOTE] +> 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. 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. + +> [!CAUTION] +> 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/trusted-dealer.md b/book/src/tutorial/trusted-dealer.md index ee07008..b17eb5d 100644 --- a/book/src/tutorial/trusted-dealer.md +++ b/book/src/tutorial/trusted-dealer.md @@ -27,55 +27,50 @@ their signing share, verifying share and group verifying key. This is done with {{#include ../../../frost-ristretto255/README.md:tkg_verify}} ``` -```admonish info -Check the crate documentation for a [full working example](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/index.html#example-key-generation-with-trusted-dealer-and-frost-signing); keep in mind it's an artificial -one since everything runs in the same program. -``` - -```admonish info -You can specify which identifiers to use by using [`IdentifierList::Custom`](https://docs.rs/frost-core/latest/frost_core/frost/keys/enum.IdentifierList.html#variant.Custom). Refer to the [DKG](dkg.md#part-1) section for an example on how to create identifiers. -``` - -```admonish danger -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 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; - -Refer to the [Terminology page](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) -for more details. - -Failure of using a **confidential** channel may lead to the shares being -stolen and possibly allowing signature forgeries if a threshold number of -them are stolen. - -Failure of using an **authenticated** channel may lead to shares being -sent to the wrong person, possibly allowing unintended parties -to generate signatures. -``` - -```admonish danger -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 - contents can only be opened with the user's password or biometrics. -``` - -```admonish warning -The participants may wish to not fully trust the dealer. While **the dealer -has access to the original secret and can forge signatures -by simply using the secret to sign** (and this can't be -possibly avoided with this method; use Distributed Key Generation -if that's an issue), the dealer could also tamper with the `SecretShare`s -in a way that the participants will never be able to generate a valid -signature in the future (denial of service). Participants can detect -such tampering by comparing the `VerifiableSecretSharingCommitment` -values from their `SecretShare`s (either by some manual process, or -by using a [broadcast channel](https://frost.zfnd.org/terminology.html#broadcast-channel)) -to make sure they are all equal. -``` +> [!TIP] +> Check the crate documentation for a [full working example](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/index.html#example-key-generation-with-trusted-dealer-and-frost-signing); keep in mind it's an artificial +> one since everything runs in the same program. + +> [!TIP] +> You can specify which identifiers to use by using [`IdentifierList::Custom`](https://docs.rs/frost-core/latest/frost_core/frost/keys/enum.IdentifierList.html#variant.Custom). Refer to the [DKG](dkg.md#part-1) section for an example on how to create identifiers. + +> [!CAUTION] +> 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 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; +> +> Refer to the [Terminology page](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) +> for more details. +> +> Failure of using a **confidential** channel may lead to the shares being +> stolen and possibly allowing signature forgeries if a threshold number of +> them are stolen. +> +> Failure of using an **authenticated** channel may lead to shares being +> sent to the wrong person, possibly allowing unintended parties +> to generate signatures. + +> [!CAUTION] +> 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 +> contents can only be opened with the user's password or biometrics. + +> [!WARNING] +> The participants may wish to not fully trust the dealer. While **the dealer +> has access to the original secret and can forge signatures +> by simply using the secret to sign** (and this can't be +> possibly avoided with this method; use Distributed Key Generation +> if that's an issue), the dealer could also tamper with the `SecretShare`s +> in a way that the participants will never be able to generate a valid +> signature in the future (denial of service). Participants can detect +> such tampering by comparing the `VerifiableSecretSharingCommitment` +> values from their `SecretShare`s (either by some manual process, or +> by using a [broadcast channel](https://frost.zfnd.org/terminology.html#broadcast-channel)) +> to make sure they are all equal. diff --git a/book/src/user/serialization.md b/book/src/user/serialization.md index 70c5389..1cfd734 100644 --- a/book/src/user/serialization.md +++ b/book/src/user/serialization.md @@ -56,11 +56,10 @@ Is encoded as - `0b`: the length of the message - `68656c6c6f20776f726c64`: the message -```admonish note -The ciphersuite ID is encoded multiple times in this case because `SigningPackage` includes -`SigningCommitments`, which also need to be communicated in Round 1 and thus also encodes -its ciphersuite ID. This is the only instance where this happens. -``` +> [!NOTE] +> The ciphersuite ID is encoded multiple times in this case because `SigningPackage` includes +> `SigningCommitments`, which also need to be communicated in Round 1 and thus also encodes +> its ciphersuite ID. This is the only instance where this happens. ## Test Vectors diff --git a/book/src/user/zeroization.md b/book/src/user/zeroization.md new file mode 100644 index 0000000..a8647fd --- /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 0000000..36aa763 --- /dev/null +++ b/book/src/zcash/devtool-demo.md @@ -0,0 +1,329 @@ +# 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. + +> [!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". + + +> [!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 +``` + +> [!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. + +> [!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 0000000..50ba3cc --- /dev/null +++ b/book/src/zcash/server.md @@ -0,0 +1,359 @@ +# 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` + +> [!NOTE] +> **Polling** is not optimal. The server will support a better mechanism in the +> future. + +> [!NOTE] +> 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). + +> [!CAUTION] +> 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). + +> [!CAUTION] +> 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 56ae655..9f0c39e 100644 --- a/book/src/zcash/technical-details.md +++ b/book/src/zcash/technical-details.md @@ -74,30 +74,28 @@ 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). -```admonish note -Orchard is simpler to handle, so it may be a good idea to just -support it with FROST. -``` +> [!NOTE] +> Orchard is simpler to handle, so it may be a good idea to just +> support it with FROST. -```admonish note -The only secret information is the key share. So another possibility -is to just ask the user to backup it (using a seed phrase format, or other -string encoding) and get the remaining information from the other participants -when recovering a wallet. -``` +> [!NOTE] +> The only secret information is the key share. So another possibility +> is to just ask the user to backup it (using a seed phrase format, or other +> string encoding) and get the remaining information from the other participants +> when recovering a wallet. ## Communications 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 ea4f134..cece551 100644 --- a/book/src/zcash/ywallet-demo.md +++ b/book/src/zcash/ywallet-demo.md @@ -1,7 +1,8 @@ # Ywallet Demo Tutorial This tutorial explaining how to run the FROST demo using Ywallet that was -[presented during Zcon4](https://www.youtube.com/watch?v=xvzESdDtczo). +[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 @@ -11,87 +12,210 @@ 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 repository: +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): ``` -git clone https://github.com/ZcashFoundation/frost-zcash-demo.git +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. + +> [!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 -- -C 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 ``` -This will by default generate a 2-of-3 key shares. The public key package -will be written to `public-key-package.json`, while key packages will be -written to `key-package-1.json` through `-3`. You can change the threshold, -number of shares and file names using the command line; append `-- -h` -to the command above for the command line help. +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". + + +> [!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. -```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: -`cargo run --bin dkg -- -C redpallas` +### Initializing config files -and follow the instructions. (There will be a considerable amount of -copy&pasting!) +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 +``` + +> [!NOTE] +> A future version might not require specifying the threshold and group name. + + ## Generating the Full Viewing Key for the wallet -Get the `verifying_key` value that is listed inside the Public Key Package in -`public-key-package.json`. For example, in the following package +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: ``` -{"verifying_shares": ...snip... ,"verifying_key":"d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b","ciphersuite":"FROST(Pallas, BLAKE2b-512)"} +frost-client groups ``` -you would need to copy -`d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b`. +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. ``` -cd zcash-sign/ -cargo run --release -- generate --ak --danger-dummy-sapling +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". + ## 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/)). -```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 -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. -**If you send it to the Sapling address, the funds will be unspendable and lost!** -``` +> [!CAUTION] +> 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 @@ -102,112 +226,83 @@ 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.raw` as suggested by Ywallet). +button and save the file somewhere (e.g. `tx-plan.json`). + ## 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, `` 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 -- sign --tx-plan --ufvk -o +zcash-sign sign --tx-plan --ufvk -o ``` -The program will print a SIGHASH and a Randomizer. - +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. -### Running the server -Now you will need to simulate two participants and a Coordinator to sign the -transaction, and the FROST server that handles communications between them. -It's probably easier to open four terminals. +### Coordinator -In the first one, the server, 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: ``` -RUST_LOG=debug cargo run --bin server +frost-client groups -c alice.toml ``` -### Registering users - -In order to interact with the server, you will need to register users. For this -guide we will need two. In a new terminal, run the following command for user -"alice" (replace the password if you want): +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). ``` -curl --data-binary '{"username": "alice", "password": "foobar10", "pubkey": ""}' -H 'Content-Type: application/json' http://127.0.0.1:2744/register +frost-client coordinator -c alice.toml --server-url localhost:2744 --group -S , -m - -r - ``` -It will output "null". (The "pubkey" parameter is not used currently and should -be empty.) Also register user "bob": +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. -``` -curl --data-binary '{"username": "bob", "password": "foobar10", "pubkey": ""}' -H 'Content-Type: application/json' http://127.0.0.1:2744/register -``` +The tool will connect to the server and wait for the other participants. -You only need to do this once, even if you want to sign more than one -transaction. If for some reason you want to start over, close the server and -delete the `db.sqlite` file. +> [!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. -Feel free to close this terminal, or reuse it for the next step. -```admonish warning -Do not use passwords that you use in practice; use dummy ones instead. (You -shouldn't reuse passwords anyway!) For real world usage you would need to take -more care to not end up writing the password to your shell history. (In real -world usage we'd expect this to be done by applications anyway.) -``` +### Participant 1 (Alice) -### Coordinator - -In the second terminal, the Coordinator, run (in the same folder where key -generation was run): +In the third terminal, Participant 1, run the following (replacing `` +with the same group public key used in the previous command): ``` -export PW=foobar10 -cargo run --bin coordinator -- -C redpallas --http -u alice -w PW -S alice,bob -r - +frost-client participant -c alice.toml --server-url localhost:2744 --group ``` -We will use "alice" as the Coordinator, so change the value next to `export PW=` -if you used another password when registering "alice". - -And then: +(We are using "Alice" again. There's nothing stopping a Coordinator from being a +Partcipant too!) -- It should read the public key package from `public-key-package.json`. -- When prompted for the message to be signed, paste the SIGHASH printed by the - signer above (just the hex value, e.g. - ``4d065453cfa4cfb4f98dbc9cff60c4a3904ed91c523b8ef8d67d28bea7f12ea3``). -- When prompted for the randomizer, paste the randomizer printed by the signer - above (again just the hex value) -```admonish warning -If you prefer to pass the randomizer as a file by using the `--randomizer` -argument, you will need to convert it to binary format. -``` +### Participant 2 (Bob) -### Participant 1 (alice) - -In the third terminal, Participant 1, run the following: +In the fourth terminal, for Participant 2, run the following (replacing `` +again): ``` -export PW=foobar10 -cargo run --bin participant -- -C redpallas --http --key-package key-package-1.json -u alice -w PW +frost-client participant -c bob.toml --server-url localhost:2744 --group ``` -(We are using "alice" again. There's nothing stopping a Coordinator from being a -Participant too!) - -### Participant 2 (bob) - -In the fourth terminal, for Participant 2, run the following: - -``` -export PW=foobar10 -cargo run --bin participant -- -C redpallas --http --key-package key-package-2.json -u bob -w PW -``` ### Coordinator @@ -218,10 +313,12 @@ successfully. It will print the final FROST-generated signature. Hurrah! Copy it 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 -(`tx-signed.raw` if you followed the suggestion). +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 2d4016d..1fcb736 100644 --- a/frost-core/CHANGELOG.md +++ b/frost-core/CHANGELOG.md @@ -2,15 +2,121 @@ Entries are listed in reverse chronological order. + ## Unreleased +## 3.0.0 + +Refer to the `3.0.0-rc.0` entry below for the main changes if you are upgrading +from `2.x`. + +### Fixed + +* Fixed `verify_signature_share()` so that it calls the + `Ciphersuite::pre_commitment_aggregate()` hook + + +## 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 since the crates + are all `no-std`. If you enabled them, just remove the feature from the + import. +* 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. + +### Added + +* 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. +* Added `ZeroizeOnDrop` for more types. +* Added `NonceCommitment` getters and `new()` under `internals` feature. + +### Fixed + +* Fixed `dkg::round2::SecretPackage` serialization (it would previously return + an error when deserialized). Technically this is a breaking change; if somehow + your application has serialized round 2 SecretPackages in long-term storage + and you need to deserialize then, you will need to manually parse them + and discard the first element of the `commitment` field. +* Fixed the build for some specific feature combinations. + + +## 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) + `frost_core::Error::culprit()`) (#728) * Added frost-secp256k1-tr crate, allowing to generate Bitcoin Taproot - (BIP340/BIP341) compatible signatures(#730). + (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). diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index a013a9c..27642bf 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "frost-core" edition.workspace = true +rust-version.workspace = true version.workspace = true authors.workspace = true readme = "README.md" @@ -18,37 +19,41 @@ rustdoc-args = ["--cfg", "docsrs"] byteorder = { version = "1.4", default-features = false } const-crc32 = { version = "1.2.0", package = "const-crc32-nostd" } document-features.workspace = true -debugless-unwrap = "0.0.4" +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-nostd-notrait = { version = "1.0.29", default-features = false } -thiserror = { version = "2.0.3", default-features = false, optional = true } +thiserror = { version = "2.0.3", default-features = false } visibility = "0.1.0" -zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } +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] -criterion = { version = "0.5" } +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", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["dep:thiserror"] ## 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. ## The docs won't list them, you will need to check the source code. @@ -59,9 +64,7 @@ internals = [] serde = ["dep:serde", "dep:serdect"] serialization = ["serde", "dep:postcard"] # Exposes ciphersuite-generic tests for other crates to use -test-impl = ["dep:proptest", "dep:serde_json", "dep: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 aac4cf2..2fe7e68 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/benches.rs b/frost-core/src/benches.rs index f89ce4a..fd4ea73 100644 --- a/frost-core/src/benches.rs +++ b/frost-core/src/benches.rs @@ -89,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 78662df..e11d9fd 100644 --- a/frost-core/src/error.rs +++ b/frost-core/src/error.rs @@ -1,19 +1,12 @@ //! FROST Error types -#[cfg(feature = "std")] -use thiserror::Error; - -#[cfg(not(feature = "std"))] -use thiserror_nostd_notrait::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")] @@ -70,7 +63,7 @@ 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.")] @@ -121,24 +114,18 @@ 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 { - culprit: identifier, - } => *identifier, + 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 @@ -165,7 +152,7 @@ where | Error::IncorrectNumberOfCommitments | Error::SerializationError | Error::DeserializationError - | Error::IdentifierDerivationNotSupported => None, + | Error::IdentifierDerivationNotSupported => vec![], } } } diff --git a/frost-core/src/keys.rs b/frost-core/src/keys.rs index d146115..5389f5e 100644 --- a/frost-core/src/keys.rs +++ b/frost-core/src/keys.rs @@ -1,5 +1,7 @@ //! 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 core::iter; @@ -15,7 +17,7 @@ use derive_getters::Getters; use hex::FromHex; use rand_core::{CryptoRng, RngCore}; -use zeroize::{DefaultIsZeroes, Zeroize}; +use zeroize::{DefaultIsZeroes, Zeroize, ZeroizeOnDrop}; use crate::{ serialization::{SerializableElement, SerializableScalar}, @@ -327,6 +329,11 @@ where .collect::>>() } + /// 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> @@ -342,6 +349,25 @@ where 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> { @@ -355,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, @@ -368,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))] @@ -524,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 { @@ -534,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) @@ -594,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))] @@ -678,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(), }) } } @@ -688,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 { @@ -700,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 @@ -707,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, } } @@ -733,6 +791,7 @@ where Ok(PublicKeyPackage::new( verifying_keys, VerifyingKey::from_commitment(commitment)?, + Some(commitment.min_signers()), )) } @@ -749,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")] diff --git a/frost-core/src/keys/dkg.rs b/frost-core/src/keys/dkg.rs index 4373a91..016490f 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]_. @@ -54,7 +58,7 @@ use super::{ pub mod round1 { use alloc::vec::Vec; use derive_getters::Getters; - use zeroize::Zeroize; + use zeroize::{Zeroize, ZeroizeOnDrop}; use super::*; @@ -117,18 +121,20 @@ pub mod round1 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq, Getters)] + #[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) #[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, @@ -196,23 +202,12 @@ pub mod round1 { .finish() } } - - impl Zeroize for SecretPackage - where - C: Ciphersuite, - { - fn zeroize(&mut self) { - for c in self.coefficients.iter_mut() { - *c = SerializableScalar(<::Field>::zero()); - } - } - } } /// DKG Round 2 structures. pub mod round2 { use derive_getters::Getters; - use zeroize::Zeroize; + use zeroize::{Zeroize, ZeroizeOnDrop}; #[cfg(feature = "serialization")] use alloc::vec::Vec; @@ -228,7 +223,7 @@ pub mod round2 { /// # Security /// /// The package must be sent on an *confidential* and *authenticated* channel. - #[derive(Clone, Debug, PartialEq, Eq, Getters)] + #[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))] @@ -275,14 +270,16 @@ pub mod round2 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq, Getters)] + #[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)). #[getter(skip)] @@ -351,15 +348,6 @@ pub mod round2 { .finish() } } - - impl Zeroize for SecretPackage - where - C: Ciphersuite, - { - fn zeroize(&mut self) { - self.secret_share = SerializableScalar(<::Field>::zero()); - } - } } /// Performs the first part of the distributed key generation protocol @@ -446,9 +434,7 @@ pub(crate) fn compute_proof_of_knowledge // > a context string to prevent replay attacks. let (k, R_i) = ::generate_nonce(&mut rng); let c_i = challenge::(identifier, &commitment.verifying_key()?, &R_i)?; - let a_i0 = *coefficients - .first() - .expect("coefficients must have at least one element"); + 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 }) } @@ -484,7 +470,7 @@ pub(crate) fn verify_proof_of_knowledge( /// `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 +/// participant has between communication channels and participants, i.e. they /// must have assurance that the [`round1::Package`] came from the participant /// with that identifier. /// @@ -506,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); } } @@ -542,7 +532,7 @@ pub fn part2( Ok(( round2::SecretPackage::new( secret_package.identifier, - secret_package.commitment, + secret_package.commitment.clone(), fii, secret_package.min_signers, secret_package.max_signers, @@ -561,7 +551,7 @@ pub fn 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 +/// participant has between communication channels and participants, i.e. they /// must have assurance that the [`round2::Package`] came from the participant /// with that identifier. /// @@ -576,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); } diff --git a/frost-core/src/keys/refresh.rs b/frost-core/src/keys/refresh.rs index ef57c60..cd80ad0 100644 --- a/frost-core/src/keys/refresh.rs +++ b/frost-core/src/keys/refresh.rs @@ -1,8 +1,27 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! 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; @@ -21,22 +40,39 @@ use core::iter; use super::{dkg::round1::Package, KeyPackage, SecretShare, VerifiableSecretSharingCommitment}; -/// Generates new zero key shares and a public key package using a trusted -/// dealer Building a new public key package is done by taking the verifying -/// shares from the new public key package and adding them to the original -/// verifying shares +/// 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, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], rng: &mut R, ) -> Result<(Vec>, PublicKeyPackage), Error> { - // Validate inputs - if identifiers.len() != max_signers as usize { - return Err(Error::IncorrectNumberOfIdentifiers); + 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); } - validate_num_of_signers(min_signers, max_signers)?; // Build refreshing shares let refreshing_key = SigningKey { @@ -46,7 +82,7 @@ pub fn compute_refreshing_shares( let coefficients = generate_coefficients::(min_signers as usize - 1, rng); let refreshing_shares = generate_secret_shares( &refreshing_key, - max_signers, + signers, min_signers, coefficients, identifiers, @@ -81,14 +117,17 @@ pub fn compute_refreshing_shares( 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)) } -/// Each participant refreshes their shares This is done by taking the -/// `refreshing_share` received from the trusted dealer and adding it to the -/// original share +/// 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, @@ -108,6 +147,10 @@ pub fn refresh_share( // 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(), @@ -119,9 +162,21 @@ pub fn refresh_share( Ok(new_key_package) } -/// Part 1 of refresh share with DKG. A refreshing_key is generated and a new package and secret_package are generated. -/// The identity commitment is removed from the packages. -pub fn refresh_dkg_part_1( +/// 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, @@ -164,7 +219,21 @@ pub fn refresh_dkg_part_1( Ok((secret_package, package)) } -/// Part 2 of refresh share with DKG. The identity commitment needs to be added back into the secret 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>, @@ -229,10 +298,12 @@ pub fn refresh_dkg_part2( } 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, + secret_package.commitment.clone(), fii, secret_package.min_signers, secret_package.max_signers, @@ -241,8 +312,28 @@ pub fn refresh_dkg_part2( )) } -/// This is the step that actually refreshes the shares. New public key packages -/// and key packages are created. +/// 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>, @@ -250,6 +341,21 @@ pub fn refresh_dkg_shares( 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 { @@ -362,6 +468,7 @@ pub fn refresh_dkg_shares( 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 { diff --git a/frost-core/src/keys/repairable.rs b/frost-core/src/keys/repairable.rs index df0be45..0d62636 100644 --- a/frost-core/src/keys/repairable.rs +++ b/frost-core/src/keys/repairable.rs @@ -1,40 +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`. +//! 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 + } + + /// 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 + } -/// 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() + } +} + +/// 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); @@ -42,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 @@ -51,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.to_scalar(); + 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(); @@ -74,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. -/// -/// 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 { +/// 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 -// -// `helper_j` sends 1 `sigma_j` to the `participant` repairing their share. - -/// Step 3 of RTS +/// 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). /// -/// 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::new(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 76078d9..364beab 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![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)] @@ -6,7 +6,6 @@ #![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!()] @@ -216,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 @@ -272,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 } } @@ -550,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 @@ -566,6 +565,40 @@ pub fn aggregate( 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, { @@ -575,12 +608,22 @@ where 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); } @@ -591,7 +634,9 @@ where // binding factor. let binding_factor_list: BindingFactorList = compute_binding_factor_list(&signing_package, &pubkeys.verifying_key, &[])?; + // Compute the group commitment from signing commitments produced in round one. + 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 @@ -619,32 +664,36 @@ 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 verification_result.is_err() { - detect_cheater( - &group_commitment, - &pubkeys, - &signing_package, - &signature_shares, - &binding_factor_list, - )?; + 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, + )?; + } + } } - #[cfg(not(feature = "cheater-detection"))] - verification_result?; - Ok(signature) } /// Optional cheater detection feature /// Each share is verified to find the cheater -#[cfg(feature = "cheater-detection")] 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( @@ -653,6 +702,8 @@ fn detect_cheater( 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])_, @@ -662,7 +713,7 @@ fn detect_cheater( .get(identifier) .ok_or(Error::UnknownIdentifier)?; - verify_signature_share_precomputed( + let r = verify_signature_share_precomputed( *identifier, signing_package, binding_factor_list, @@ -670,7 +721,22 @@ fn detect_cheater( 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, + }); } // We should never reach here; but we return an error to be safe. @@ -682,7 +748,7 @@ fn detect_cheater( /// for which the signature share was produced and with the group's /// `verifying_key`. /// -/// This is not required for regular FROST usage but might useful in certain +/// 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( @@ -695,7 +761,15 @@ pub fn verify_signature_share( // 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::new(verifying_shares, *verifying_key); + 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)?; @@ -704,17 +778,19 @@ pub fn verify_signature_share( let verifying_share = pubkeys .verifying_shares() .get(&identifier) - .expect("pre_aggregate() must keep the identifiers"); + .ok_or(Error::UnknownIdentifier)?; let verifying_key = pubkeys.verifying_key(); let signature_share = signature_shares .get(&identifier) - .expect("pre_aggregate() must keep the identifiers"); + .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)?; diff --git a/frost-core/src/round1.rs b/frost-core/src/round1.rs index 88cdeac..d2468fd 100644 --- a/frost-core/src/round1.rs +++ b/frost-core/src/round1.rs @@ -1,4 +1,6 @@ //! FROST Round 1 functionality and types +// Remove after https://github.com/rust-lang/rust/issues/147648 is fixed +#![allow(unused_assignments)] use alloc::{ collections::BTreeMap, @@ -12,7 +14,7 @@ use derive_getters::Getters; use hex::FromHex; use rand_core::{CryptoRng, RngCore}; -use zeroize::Zeroize; +use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ serialization::{SerializableElement, SerializableScalar}, @@ -131,10 +133,15 @@ where C: Ciphersuite, { /// 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)) } + /// 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 } @@ -202,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))] @@ -389,6 +396,8 @@ impl GroupCommitmentShare { /// commitment list. /// /// [`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>, ) -> Result, Error> { diff --git a/frost-core/src/round2.rs b/frost-core/src/round2.rs index d3147db..b7a1e90 100644 --- a/frost-core/src/round2.rs +++ b/frost-core/src/round2.rs @@ -74,7 +74,7 @@ where + (verifying_share.to_element() * challenge.0 * lambda_i)) { return Err(Error::InvalidSignatureShare { - culprit: identifier, + culprits: vec![identifier], }); } @@ -155,6 +155,8 @@ pub fn sign( .clone(); // Compute the group commitment from signing commitments produced in round one. + 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. diff --git a/frost-core/src/scalar_mul.rs b/frost-core/src/scalar_mul.rs index e8215c4..3d82b28 100644 --- a/frost-core/src/scalar_mul.rs +++ b/frost-core/src/scalar_mul.rs @@ -14,27 +14,6 @@ use alloc::vec::Vec; use crate::{Ciphersuite, Element, Field, Group, Scalar}; -/// 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 - } -} - /// A trait for transforming a scalar generic over a ciphersuite to a non-adjacent form (NAF). pub trait NonAdjacentForm { fn non_adjacent_form(&self, w: usize) -> Vec; @@ -81,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]; diff --git a/frost-core/src/serialization.rs b/frost-core/src/serialization.rs index fe1df7b..8e310fd 100644 --- a/frost-core/src/serialization.rs +++ b/frost-core/src/serialization.rs @@ -1,8 +1,21 @@ //! Serialization support. +#[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}; @@ -27,15 +40,22 @@ where /// Deserialize a Scalar from a serialized buffer. pub fn deserialize(bytes: &[u8]) -> Result> { - let serialized: <::Field as Field>::Serialization = bytes - .to_vec() - .try_into() - .map_err(|_| FieldError::MalformedScalar)?; + 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 SerializableScalar where @@ -59,18 +79,13 @@ 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"))?; - <::Group as Group>::Field::deserialize(&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) } @@ -91,10 +106,8 @@ where /// Deserialize an Element. Returns an error if it's malformed or is the /// identity. pub fn deserialize(bytes: &[u8]) -> Result> { - let serialized: ::Serialization = bytes - .to_vec() - .try_into() - .map_err(|_| FieldError::MalformedScalar)?; + let serialized: ::Serialization = + bytes.try_into().map_err(|_| FieldError::MalformedScalar)?; let scalar = ::deserialize(&serialized)?; Ok(Self(scalar)) } @@ -124,19 +137,14 @@ 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) - .expect("serializing the generator always works") - .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"))?; - ::deserialize(&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) } @@ -244,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 9152769..02ccaac 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -38,34 +38,31 @@ where // 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(); + let mut R_serialization = ::serialize(&generator)?; + let R_bytes_len = R_serialization.as_ref().len(); - let one = <::Field as Field>::zero(); - let mut z_bytes = - Vec::from(<::Field as Field>::serialize(&one).as_ref()); - let z_bytes_len = z_bytes.len(); + let zero = <::Field as Field>::zero(); + let mut z_serialization = <::Field as Field>::serialize(&zero); + let z_bytes_len = z_serialization.as_ref().len(); if bytes.len() != R_bytes_len + z_bytes_len { return Err(Error::MalformedSignature); } - R_bytes[..].copy_from_slice(bytes.get(0..R_bytes_len).ok_or(Error::MalformedSignature)?); - - let R_serialization = &R_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + 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 .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)?, }) } @@ -77,10 +74,16 @@ where /// Converts this signature to its default byte serialization. #[cfg_attr(feature = "internals", visibility::make(pub))] pub(crate) fn default_serialize(&self) -> Result, Error> { - let mut bytes = Vec::::new(); + 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); Ok(bytes) } diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs index 574bf96..20aa634 100644 --- a/frost-core/src/signing_key.rs +++ b/frost-core/src/signing_key.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; +use zeroize::ZeroizeOnDrop; use crate::{ random_nonzero, serialization::SerializableScalar, Challenge, Ciphersuite, Error, Field, Group, @@ -10,7 +11,7 @@ use crate::{ }; /// 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, @@ -52,7 +53,7 @@ where mut rng: R, message: &[u8], ) -> Signature { - let public = VerifyingKey::::from(*self); + let public = VerifyingKey::::from(self.clone()); let (k, R) = ::generate_nonce(&mut rng); @@ -80,6 +81,17 @@ where } } +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, diff --git a/frost-core/src/tests/ciphersuite_generic.rs b/frost-core/src/tests/ciphersuite_generic.rs index 220bfb5..57349f8 100644 --- a/frost-core/src/tests/ciphersuite_generic.rs +++ b/frost-core/src/tests/ciphersuite_generic.rs @@ -1,11 +1,14 @@ //! Ciphersuite-generic test functions. #![allow(clippy::type_complexity)] +#![cfg(feature = "serialization")] use alloc::{borrow::ToOwned, collections::BTreeMap, vec::Vec}; use rand_core::{CryptoRng, RngCore}; use crate as frost; -use crate::keys::SigningShare; +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, SigningPackage, @@ -25,6 +28,8 @@ pub fn check_zero_key_fails() { /// 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; @@ -109,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 @@ -145,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. @@ -204,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( @@ -214,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); } @@ -224,6 +244,9 @@ pub fn check_sign( let mut signature_shares = BTreeMap::new(); let message = "message to sign".as_bytes(); 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 @@ -242,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); } @@ -266,6 +291,8 @@ pub fn check_sign( // 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). @@ -315,14 +342,6 @@ fn check_aggregate_errors( signature_shares: BTreeMap, frost::round2::SignatureShare>, pubkey_package: frost::keys::PublicKeyPackage, ) { - #[cfg(not(feature = "cheater-detection"))] - let pubkey_package = PublicKeyPackage { - header: pubkey_package.header, - verifying_shares: BTreeMap::new(), - verifying_key: pubkey_package.verifying_key, - }; - - #[cfg(feature = "cheater-detection")] check_aggregate_corrupted_share( signing_package.clone(), signature_shares.clone(), @@ -336,22 +355,70 @@ fn check_aggregate_errors( ); } -#[cfg(feature = "cheater-detection")] fn check_aggregate_corrupted_share( signing_package: frost::SigningPackage, mut signature_shares: BTreeMap, frost::round2::SignatureShare>, pubkey_package: frost::keys::PublicKeyPackage, ) { - use crate::round2::SignatureShare; + 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() = - SignatureShare::new(signature_shares[&id].to_scalar() + 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). @@ -412,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 @@ -429,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(), + ); } } @@ -459,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 @@ -469,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(), + ); } } @@ -485,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. @@ -515,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) @@ -528,10 +641,17 @@ 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() @@ -729,7 +849,7 @@ fn check_part2_error( 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 }); } @@ -738,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 @@ -930,8 +1050,8 @@ fn check_verifying_shares( SignatureShare::new(signature_shares[&id].to_scalar() + one); let e = frost::aggregate(&signing_package, &signature_shares, &pubkeys).unwrap_err(); - assert_eq!(e.culprit(), Some(id)); - assert_eq!(e, Error::InvalidSignatureShare { culprit: id }); + assert_eq!(e.culprits(), vec![id]); + assert_eq!(e, Error::InvalidSignatureShare { culprits: vec![id] }); } // Checks if `verify_signature_share()` works correctly. @@ -966,3 +1086,89 @@ fn check_verify_signature_share( .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 b3392ca..b491108 100644 --- a/frost-core/src/tests/coefficient_commitment.rs +++ b/frost-core/src/tests/coefficient_commitment.rs @@ -2,7 +2,7 @@ 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; @@ -44,7 +44,7 @@ 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(); diff --git a/frost-core/src/tests/refresh.rs b/frost-core/src/tests/refresh.rs index 384924e..8f2d56f 100644 --- a/frost-core/src/tests/refresh.rs +++ b/frost-core/src/tests/refresh.rs @@ -1,15 +1,18 @@ //! 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_part2, refresh_dkg_part_1, refresh_share, + compute_refreshing_shares, refresh_dkg_part1, refresh_dkg_part2, refresh_share, }; -#[cfg(feature = "serialization")] -use crate::keys::{PublicKeyPackage, SecretShare}; use crate::{self as frost}; -use crate::{keys::KeyPackage, Ciphersuite, Error, Identifier, Signature, VerifyingKey}; +use crate::{ + keys::{KeyPackage, PublicKeyPackage, SecretShare}, + Ciphersuite, Error, Identifier, Signature, VerifyingKey, +}; use crate::tests::ciphersuite_generic::check_part3_different_participants; @@ -18,7 +21,7 @@ use alloc::vec::Vec; use super::ciphersuite_generic::check_sign; -/// 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_refresh_shares_with_dealer(mut rng: R) { // Compute shares @@ -56,18 +59,14 @@ pub fn check_refresh_shares_with_dealer( Identifier::try_from(5).unwrap(), ]; - const NEW_MAX_SIGNERS: u16 = 4; - // Trusted Dealer generates zero keys and new public key package - let (zero_shares, new_pub_key_package) = compute_refreshing_shares( - pub_key_package, - NEW_MAX_SIGNERS, - MIN_SIGNERS, - &remaining_ids, - &mut rng, - ) - .unwrap(); + 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 @@ -76,14 +75,18 @@ pub fn check_refresh_shares_with_dealer( for i in 0..remaining_ids.len() { let identifier = remaining_ids[i]; let current_share = &old_key_packages[&identifier]; - let new_share = refresh_share(zero_shares[i].clone(), current_share); + // 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 { - key_packages.insert(k, v.unwrap()); + // 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(); } @@ -93,21 +96,13 @@ pub fn check_refresh_shares_with_dealer_fails_with_invalid_signers< C: Ciphersuite, R: RngCore + CryptoRng, >( - new_max_signers: u16, - min_signers: u16, 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, - new_max_signers, - min_signers, - identifiers, - &mut rng, - ); + let out = compute_refreshing_shares(pub_key_package, identifiers, &mut rng); assert!(out.is_err()); assert!(out == Err(error)) @@ -157,18 +152,10 @@ pub fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package< Identifier::try_from(5).unwrap(), ]; - const NEW_MAX_SIGNERS: u16 = 4; - // Trusted Dealer generates zero keys and new public key package - let e = compute_refreshing_shares( - incorrect_pub_key_package, - NEW_MAX_SIGNERS, - MIN_SIGNERS, - &remaining_ids, - &mut rng, - ) - .unwrap_err(); + let e = + compute_refreshing_shares(incorrect_pub_key_package, &remaining_ids, &mut rng).unwrap_err(); assert_eq!(e, Error::UnknownIdentifier) } @@ -206,16 +193,8 @@ pub fn check_refresh_shares_with_dealer_serialisation::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 @@ -323,7 +317,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(), + ); } } @@ -350,9 +348,20 @@ where 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, 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 @@ -360,10 +369,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(), + ); } } @@ -376,9 +394,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. @@ -408,7 +426,13 @@ where old_key_packages[&participant_identifier].clone(), ) .unwrap(); - verifying_keys.insert(participant_identifier, key_package.verifying_share); + // 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) @@ -421,11 +445,166 @@ 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() } + +/// 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 532e1e0..e437973 100644 --- a/frost-core/src/tests/repairable.rs +++ b/frost-core/src/tests/repairable.rs @@ -2,21 +2,23 @@ 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,7 +153,7 @@ 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.to_scalar(); @@ -147,70 +161,77 @@ pub fn check_repair_share_step_1(mut rng 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::new(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 c11b144..1ca8a97 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 alloc::collections::BTreeMap; -use debugless_unwrap::DebuglessUnwrap; +use debugless_unwrap::DebuglessUnwrapExt; use hex::{self, FromHex}; use serde_json::Value; @@ -45,7 +45,8 @@ pub fn parse_test_vectors(json_vectors: &Value) -> TestVectors::Field>::deserialize(&vec.try_into().debugless_unwrap()).unwrap() + <::Field>::deserialize(&vec.as_slice().try_into().debugless_unwrap()) + .unwrap() }) .collect(); @@ -292,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, diff --git a/frost-core/src/tests/vectors_dkg.rs b/frost-core/src/tests/vectors_dkg.rs index 894eda2..6f1ce53 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 alloc::{collections::BTreeMap, string::ToString, vec::Vec}; - -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,88 +35,106 @@ 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"]).as_ref()).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"]).as_ref()) - .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"]).as_ref()).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"]).as_ref()) - .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() @@ -123,7 +144,7 @@ fn build_round_1_package( let commitment = VerifiableSecretSharingCommitment::deserialize(vss_commitment).unwrap(); let proof_of_knowledge = Signature::deserialize( - &hex::decode(participant["proof_of_knowledge"].as_str().unwrap()).unwrap(), + &hex::decode(json_vectors["proof_of_knowledge"].as_str().unwrap()).unwrap(), ) .debugless_unwrap(); @@ -136,12 +157,10 @@ 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()]).as_ref(), + json_to_scalar::(&json_vectors["signing_shares"][sender_num]).as_ref(), ) .unwrap(); @@ -157,6 +176,7 @@ 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() @@ -177,46 +197,49 @@ fn build_public_key_package(json_vectors: &Value) -> PublicKeyPa 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::new( - participant_id, - coefficients, - 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 1046522..62a2755 100644 --- a/frost-core/src/tests/vss_commitment.rs +++ b/frost-core/src/tests/vss_commitment.rs @@ -3,9 +3,10 @@ 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; @@ -46,6 +47,40 @@ pub fn check_serialize_vss_commitment(mu .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 pub fn check_deserialize_vss_commitment(mut rng: R) { // Generate test CoefficientCommitments @@ -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,7 +161,7 @@ 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(); // --- @@ -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 91a5056..9ae9431 100644 --- a/frost-core/src/traits.rs +++ b/frost-core/src/traits.rs @@ -10,12 +10,12 @@ use rand_core::{CryptoRng, RngCore}; use crate::{ challenge, - keys::{KeyPackage, PublicKeyPackage, VerifyingShare}, + keys::{KeyPackage, PublicKeyPackage, SecretShare, VerifyingShare}, random_nonzero, - round1::{self}, + round1::{self, SigningNonces}, round2::{self, SignatureShare}, - BindingFactor, Challenge, Error, FieldError, GroupCommitment, GroupError, Identifier, - Signature, SigningKey, SigningPackage, VerifyingKey, + 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 @@ -25,7 +25,7 @@ use crate::{ /// 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 @@ -34,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; @@ -85,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; @@ -97,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. @@ -147,7 +151,7 @@ pub type Element = <::Group as Group>::Element; /// /// [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 + Clone + PartialEq + Debug + 'static { +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 @@ -162,7 +166,11 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { /// 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. /// @@ -287,9 +295,9 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { /// 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. The default implementation returns them as-is. [`Cow`] is used - /// so implementations can choose to return the same passed reference or a - /// modified clone. + /// 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, @@ -333,6 +341,29 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { )) } + /// 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. @@ -422,4 +453,19 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { ) -> Result<(KeyPackage, PublicKeyPackage), Error> { Ok((key_package, public_key_package)) } + + /// 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-ed25519/CHANGELOG.md b/frost-ed25519/CHANGELOG.md new file mode 100644 index 0000000..b1dedcb --- /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 b6d26d5..8a07396 100644 --- a/frost-ed25519/Cargo.toml +++ b/frost-ed25519/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "frost-ed25519" edition.workspace = true - +rust-version.workspace = true version.workspace = true authors.workspace = true readme = "README.md" @@ -18,15 +18,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } document-features.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", 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.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +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 @@ -35,21 +35,17 @@ proptest.workspace = true rand.workspace = true rand_chacha.workspace = true serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ed25519/README.md b/frost-ed25519/README.md index f9f267c..500f8f4 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( diff --git a/frost-ed25519/benches/bench.rs b/frost-ed25519/benches/bench.rs index 04cfbfb..4317e05 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 19e2433..244d9ba 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 a 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/refresh.rs b/frost-ed25519/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-ed25519/src/keys/refresh.rs +++ b/frost-ed25519/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 5e875a7..9e809ed 100644 --- a/frost-ed25519/src/keys/repairable.rs +++ b/frost-ed25519/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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 9c4bce8..cb7a818 100644 --- a/frost-ed25519/src/lib.rs +++ b/frost-ed25519/src/lib.rs @@ -1,7 +1,6 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![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!()] @@ -145,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 } @@ -419,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 0000000..7a90a0d --- /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 2649793..a57301f 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 113eb1c..d45994e 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 2bd1bae..3e0fcd2 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 9336302..db17392 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/samples.rs b/frost-ed25519/tests/helpers/samples.rs index e1d87e1..2c6c9c5 100644 --- a/frost-ed25519/tests/helpers/samples.rs +++ b/frost-ed25519/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-ed25519/tests/helpers/vectors_dkg.json b/frost-ed25519/tests/helpers/vectors_dkg.json index bcad1c9..4284fe4 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 c8c27fc..2cc6620 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( rng, @@ -82,7 +81,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< Ed25519Sha512, @@ -90,113 +89,49 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed25519Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed25519Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed25519Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed25519Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< Ed25519Sha512, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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; @@ -210,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; @@ -224,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; @@ -240,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; @@ -260,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; @@ -274,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; @@ -338,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, @@ -348,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, ); @@ -356,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 9c27193..e758ee2 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 5ae8964..479b7d9 100644 --- a/frost-ed25519/tests/recreation_tests.rs +++ b/frost-ed25519/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-ed25519/tests/rerandomized_tests.rs b/frost-ed25519/tests/rerandomized_tests.rs index e6981bd..1217387 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 9f72279..a36c278 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 7fe52b9..6af9016 100644 --- a/frost-ed25519/tests/serialization_tests.rs +++ b/frost-ed25519/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..342de36 --- /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-ed448/CHANGELOG.md b/frost-ed448/CHANGELOG.md new file mode 100644 index 0000000..b1dedcb --- /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 16282a9..5c26e30 100644 --- a/frost-ed448/Cargo.toml +++ b/frost-ed448/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "frost-ed448" edition.workspace = true +rust-version.workspace = true version.workspace = true authors.workspace = true readme = "README.md" @@ -17,15 +18,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features.workspace = true ed448-goldilocks = { version = "0.9.0" } -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", default-features = false } +frost-core.workspace = true +frost-rerandomized.workspace = true rand_core.workspace = true sha3 = { version = "0.10.6", default-features = false } [dev-dependencies] criterion.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } lazy_static.workspace = true insta.workspace = true hex.workspace = true @@ -33,21 +34,17 @@ proptest.workspace = true rand.workspace = true rand_chacha.workspace = true serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ed448/README.md b/frost-ed448/README.md index 7cf30ff..77f1eb8 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( diff --git a/frost-ed448/benches/bench.rs b/frost-ed448/benches/bench.rs index 343f72f..ba40feb 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 6471d76..ff304b9 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 a 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/refresh.rs b/frost-ed448/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-ed448/src/keys/refresh.rs +++ b/frost-ed448/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 97771f3..f1e21d4 100644 --- a/frost-ed448/src/keys/repairable.rs +++ b/frost-ed448/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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 5652356..154f445 100644 --- a/frost-ed448/src/lib.rs +++ b/frost-ed448/src/lib.rs @@ -1,13 +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!()] extern crate alloc; -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; use ed448_goldilocks::{ curve::{edwards::CompressedEdwardsY, ExtendedPoint}, @@ -230,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>; @@ -414,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 0000000..10d5d01 --- /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 85b6b1a..5c84b5e 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 d088ad1..0d2091a 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 98810b3..7e1f834 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 44f389a..749cc70 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.rs b/frost-ed448/tests/helpers/samples.rs index 4994b51..529633b 100644 --- a/frost-ed448/tests/helpers/samples.rs +++ b/frost-ed448/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-ed448/tests/helpers/vectors_dkg.json b/frost-ed448/tests/helpers/vectors_dkg.json index a0943e8..09a2297 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 b96d832..28bd22a 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( rng, @@ -82,7 +81,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< Ed448Shake256, @@ -90,113 +89,49 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed448Shake256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed448Shake256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed448Shake256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ed448Shake256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< Ed448Shake256, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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; @@ -210,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; @@ -224,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; @@ -240,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; @@ -260,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; @@ -274,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; @@ -338,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, @@ -348,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, ); @@ -356,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 44b7abf..e206ccf 100644 --- a/frost-ed448/tests/recreation_tests.rs +++ b/frost-ed448/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-ed448/tests/rerandomized_tests.rs b/frost-ed448/tests/rerandomized_tests.rs index 16e31f2..e16d906 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 3b5c667..db380ee 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 e6a177e..e9810a6 100644 --- a/frost-ed448/tests/serialization_tests.rs +++ b/frost-ed448/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..fcfcff4 --- /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-p256/CHANGELOG.md b/frost-p256/CHANGELOG.md new file mode 100644 index 0000000..b1dedcb --- /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 dc5e3ed..68e63b3 100644 --- a/frost-p256/Cargo.toml +++ b/frost-p256/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "frost-p256" 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", "threshold", "signature"] +keywords = ["cryptography", "crypto", "p256", "threshold", "signature"] description = "A Schnorr signature scheme over the NIST P-256 curve that supports FROST." [package.metadata.docs.rs] @@ -17,15 +18,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features.workspace = true p256 = { version = "0.13.0", features = ["hash2curve"], default-features = false } -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", 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.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } insta.workspace = true hex.workspace = true lazy_static.workspace = true @@ -33,21 +34,17 @@ proptest.workspace = true rand.workspace = true rand_chacha.workspace = true serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-p256/README.md b/frost-p256/README.md index df0cc4a..47f391f 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( diff --git a/frost-p256/benches/bench.rs b/frost-p256/benches/bench.rs index 1a4d835..8ae524f 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 f3dbb23..cd64fd9 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 a 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/refresh.rs b/frost-p256/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-p256/src/keys/refresh.rs +++ b/frost-p256/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 2337574..98db9d1 100644 --- a/frost-p256/src/keys/repairable.rs +++ b/frost-p256/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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 35c2620..42aa94f 100644 --- a/frost-p256/src/lib.rs +++ b/frost-p256/src/lib.rs @@ -1,7 +1,6 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![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!()] @@ -154,7 +153,7 @@ 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 } @@ -429,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 0000000..e5e8b44 --- /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 1d4cff1..3a46bfd 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 8083aea..e52f839 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/vss_commitment.rs b/frost-p256/src/tests/vss_commitment.rs index a30d3f6..44c08c5 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 16f52d5..8dc2d73 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.rs b/frost-p256/tests/helpers/samples.rs index be57e47..e6a2fd2 100644 --- a/frost-p256/tests/helpers/samples.rs +++ b/frost-p256/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-p256/tests/integration_tests.rs b/frost-p256/tests/integration_tests.rs index acd3f81..d1d27f4 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( rng, @@ -82,7 +81,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< P256Sha256, @@ -90,113 +89,49 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - P256Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - P256Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - P256Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - P256Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< P256Sha256, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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; @@ -210,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; @@ -224,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; @@ -240,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; @@ -260,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; @@ -274,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; @@ -336,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, @@ -345,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, ); @@ -353,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 0f4dbf7..99c39a2 100644 --- a/frost-p256/tests/recreation_tests.rs +++ b/frost-p256/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-p256/tests/rerandomized_tests.rs b/frost-p256/tests/rerandomized_tests.rs index d16a0c3..6dc482c 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 c147581..7091bd7 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 b83b12e..dbd32ce 100644 --- a/frost-p256/tests/serialization_tests.rs +++ b/frost-p256/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..158419f --- /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-rerandomized/CHANGELOG.md b/frost-rerandomized/CHANGELOG.md new file mode 100644 index 0000000..eb10fc5 --- /dev/null +++ b/frost-rerandomized/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +Entries are listed in reverse chronological order. + + +## Unreleased + +## 3.0.0 + +* No changes. + +## 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 878f710..2579900 100644 --- a/frost-rerandomized/Cargo.toml +++ b/frost-rerandomized/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "frost-rerandomized" edition.workspace = true +rust-version.workspace = true version.workspace = true authors.workspace = true readme = "README.md" @@ -17,27 +18,20 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] derive-getters = "0.5.0" document-features.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", features = [ - "internals" -], default-features = false } +frost-core = { workspace = true, features = ["internals"] } hex.workspace = true rand_core.workspace = true [dev-dependencies] [features] -nightly = [] -default = ["serialization", "cheater-detection"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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", "serialization"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection"] ## 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 733b6e4..543f13b 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 a309d6f..0b5e057 100644 --- a/frost-rerandomized/src/lib.rs +++ b/frost-rerandomized/src/lib.rs @@ -3,13 +3,16 @@ //! 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`]. -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] #![allow(non_snake_case)] extern crate alloc; @@ -27,8 +30,9 @@ use frost_core::SigningPackage; use frost_core::{ self as frost, keys::{KeyPackage, PublicKeyPackage, SigningShare, VerifyingShare}, + round1::{encode_group_commitments, SigningCommitments}, serialization::SerializableScalar, - Ciphersuite, Error, Field, Group, Scalar, VerifyingKey, + CheaterDetection, Ciphersuite, Error, Field, Group, Identifier, Scalar, VerifyingKey, }; #[cfg(feature = "serde")] @@ -36,7 +40,6 @@ use frost_core::serde; // When pulled into `reddsa`, that has its own sibling `rand_core` import. // For the time being, we do not re-export this `rand_core`. -#[cfg(feature = "serialization")] use rand_core::{CryptoRng, RngCore}; /// Randomize the given key type for usage in a FROST signing with re-randomized keys, @@ -111,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(), )) } } @@ -122,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, @@ -134,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( @@ -156,6 +181,29 @@ 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))] @@ -177,12 +225,15 @@ 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, @@ -211,6 +262,65 @@ where .ok_or(Error::SerializationError)?; 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))) + } } impl Randomizer @@ -266,18 +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 diff --git a/frost-rerandomized/src/tests.rs b/frost-rerandomized/src/tests.rs index f29c91c..0b52649 100644 --- a/frost-rerandomized/src/tests.rs +++ b/frost-rerandomized/src/tests.rs @@ -6,7 +6,9 @@ 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. @@ -70,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( @@ -138,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); @@ -176,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 0000000..b1dedcb --- /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 ff67d89..3233dc4 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "frost-ristretto255" edition.workspace = true +rust-version.workspace = true version.workspace = true authors.workspace = true readme = "README.md" @@ -17,15 +18,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } document-features.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", 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 = { version = "0.5", features = ["html_reports"] } -frost-core = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +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 @@ -34,21 +35,17 @@ proptest.workspace = true rand.workspace = true rand_chacha.workspace = true serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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", "curve25519-dalek/serde"] ## Enable a default serialization format. Enables `serde`. serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-ristretto255/README.md b/frost-ristretto255/README.md index a2d2842..0f061bd 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( diff --git a/frost-ristretto255/benches/bench.rs b/frost-ristretto255/benches/bench.rs index b53e560..b7e9af3 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 18954d4..fcba23b 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 a 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/refresh.rs b/frost-ristretto255/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-ristretto255/src/keys/refresh.rs +++ b/frost-ristretto255/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 a935eb8..3c316bc 100644 --- a/frost-ristretto255/src/keys/repairable.rs +++ b/frost-ristretto255/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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 0929c22..490e34c 100644 --- a/frost-ristretto255/src/lib.rs +++ b/frost-ristretto255/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] #![doc = include_str!("../README.md")] @@ -22,6 +22,8 @@ use frost_core as frost; #[cfg(test)] mod tests; +pub mod rerandomized; + // Re-exports in our public API #[cfg(feature = "serde")] pub use frost_core::serde; @@ -131,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 } @@ -405,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 0000000..21410f4 --- /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 9c08b77..b26d033 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 e5df351..a031d4c 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 d7acaaa..06b16e4 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 a985d0a..8d1fcf2 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.rs b/frost-ristretto255/tests/helpers/samples.rs index d6f58b9..b0bf586 100644 --- a/frost-ristretto255/tests/helpers/samples.rs +++ b/frost-ristretto255/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-ristretto255/tests/integration_tests.rs b/frost-ristretto255/tests/integration_tests.rs index 7436202..c4ef3be 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< Ristretto255Sha512, @@ -83,7 +82,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< Ristretto255Sha512, @@ -91,113 +90,50 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ristretto255Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ristretto255Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ristretto255Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Ristretto255Sha512, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< Ristretto255Sha512, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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; @@ -211,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; @@ -225,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; @@ -241,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; @@ -261,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; @@ -275,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; @@ -339,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, @@ -349,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, _, @@ -358,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 a5974a9..0d8e516 100644 --- a/frost-ristretto255/tests/recreation_tests.rs +++ b/frost-ristretto255/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-ristretto255/tests/rerandomized_tests.rs b/frost-ristretto255/tests/rerandomized_tests.rs index a7a884c..23277d0 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 faf1769..d73e846 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 1aa96a2..641bb47 100644 --- a/frost-ristretto255/tests/serialization_tests.rs +++ b/frost-ristretto255/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..8a5fd31 --- /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-secp256k1-tr/CHANGELOG.md b/frost-secp256k1-tr/CHANGELOG.md new file mode 100644 index 0000000..b1dedcb --- /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 index 3a2292c..ad45c8f 100644 --- a/frost-secp256k1-tr/Cargo.toml +++ b/frost-secp256k1-tr/Cargo.toml @@ -1,13 +1,14 @@ [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", "threshold", "signature"] +keywords = ["cryptography", "crypto", "secp256k1", "threshold", "signature"] description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST and Taproot." [package.metadata.docs.rs] @@ -16,39 +17,35 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", default-features = false } +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 = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +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.30.0" +secp256k1 = "0.31.0" serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-secp256k1-tr/README.md b/frost-secp256k1-tr/README.md index f4d2205..bc74f3e 100644 --- a/frost-secp256k1-tr/README.md +++ b/frost-secp256k1-tr/README.md @@ -1,6 +1,13 @@ 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 @@ -11,10 +18,9 @@ scenario in a single thread and it abstracts away any communication between peer ```rust # // ANCHOR: tkg_gen use frost_secp256k1_tr 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( diff --git a/frost-secp256k1-tr/benches/bench.rs b/frost-secp256k1-tr/benches/bench.rs index e9097bd..d2ce56f 100644 --- a/frost-secp256k1-tr/benches/bench.rs +++ b/frost-secp256k1-tr/benches/bench.rs @@ -1,16 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use rand::thread_rng; use frost_secp256k1_tr::*; 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-tr/dkg.md b/frost-secp256k1-tr/dkg.md index 9643d37..e0be393 100644 --- a/frost-secp256k1-tr/dkg.md +++ b/frost-secp256k1-tr/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 a 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_tr 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-tr/src/keys/refresh.rs b/frost-secp256k1-tr/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-secp256k1-tr/src/keys/refresh.rs +++ b/frost-secp256k1-tr/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 index 9b53803..3ae930d 100644 --- a/frost-secp256k1-tr/src/keys/repairable.rs +++ b/frost-secp256k1-tr/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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, Secp256K1Sha256TR}; -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 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 { - 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::Secp256K1Sha256TR; @@ -70,32 +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::< Secp256K1Sha256TR, _, >(rng); diff --git a/frost-secp256k1-tr/src/lib.rs b/frost-secp256k1-tr/src/lib.rs index f566f72..daa1960 100644 --- a/frost-secp256k1-tr/src/lib.rs +++ b/frost-secp256k1-tr/src/lib.rs @@ -1,7 +1,6 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![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!()] @@ -163,7 +162,7 @@ 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 } @@ -300,7 +299,7 @@ impl Ciphersuite for Secp256K1Sha256TR { rng: R, message: &[u8], ) -> Signature { - let signing_key = signing_key.into_even_y(None); + let signing_key = signing_key.clone().into_even_y(None); signing_key.default_sign(rng, message) } @@ -600,7 +599,7 @@ pub mod keys { /// Trait for ensuring the group public key has an even Y coordinate. /// - /// In BIP-320, public keys are encoded with only the X coordinate, which + /// 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 @@ -645,7 +644,7 @@ pub mod keys { (*i, vs) }) .collect(); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal(verifying_shares, verifying_key, self.min_signers()) } else { self } @@ -766,7 +765,11 @@ pub mod keys { (*i, vs) }) .collect(); - PublicKeyPackage::new(verifying_shares, verifying_key) + PublicKeyPackage::new_internal( + verifying_shares, + verifying_key, + public_key_package.min_signers(), + ) } } diff --git a/frost-secp256k1-tr/src/rerandomized.rs b/frost-secp256k1-tr/src/rerandomized.rs new file mode 100644 index 0000000..7135cdb --- /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/batch.rs b/frost-secp256k1-tr/src/tests/batch.rs index f88793a..d22efdf 100644 --- a/frost-secp256k1-tr/src/tests/batch.rs +++ b/frost-secp256k1-tr/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-tr/src/tests/coefficient_commitment.rs b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs index a63259c..71706ea 100644 --- a/frost-secp256k1-tr/src/tests/coefficient_commitment.rs +++ b/frost-secp256k1-tr/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::< Secp256K1Sha256TR, _, @@ -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::< Secp256K1Sha256TR, _, @@ -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::< Secp256K1Sha256TR, diff --git a/frost-secp256k1-tr/src/tests/vss_commitment.rs b/frost-secp256k1-tr/src/tests/vss_commitment.rs index 80fb1ca..f264c33 100644 --- a/frost-secp256k1-tr/src/tests/vss_commitment.rs +++ b/frost-secp256k1-tr/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,29 +12,54 @@ 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::< + Secp256K1Sha256TR, + _, + >(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-tr/tests/common_traits_tests.rs b/frost-secp256k1-tr/tests/common_traits_tests.rs index 81b97a9..93265b7 100644 --- a/frost-secp256k1-tr/tests/common_traits_tests.rs +++ b/frost-secp256k1-tr/tests/common_traits_tests.rs @@ -4,7 +4,6 @@ mod helpers; use frost_secp256k1_tr::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-tr/tests/helpers/mod.rs b/frost-secp256k1-tr/tests/helpers/mod.rs index 0de6147..299e33f 100644 --- a/frost-secp256k1-tr/tests/helpers/mod.rs +++ b/frost-secp256k1-tr/tests/helpers/mod.rs @@ -17,7 +17,7 @@ pub fn verify_signature( group_signature.serialize().unwrap().try_into().unwrap(), ); let pubkey = secp256k1::XOnlyPublicKey::from_byte_array( - &group_pubkey.serialize().unwrap()[1..33].try_into().unwrap(), + 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/samples.rs b/frost-secp256k1-tr/tests/helpers/samples.rs index 337ae70..8c39a9e 100644 --- a/frost-secp256k1-tr/tests/helpers/samples.rs +++ b/frost-secp256k1-tr/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-secp256k1-tr/tests/helpers/vectors_dkg.json b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json index 9fcbc83..55755fb 100644 --- a/frost-secp256k1-tr/tests/helpers/vectors_dkg.json +++ b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json @@ -13,7 +13,7 @@ "signing_key": "68e3f6904c6043973515a36bf7801a71597da35733f21305d75a5234f06e4529", "coefficient": "25d2d840a3e2718a431ec69e14ee8a015b000d43c7a9868060f01d5aa52a19d1", "vss_commitments": ["03e7ba4acb164d2bd5eba4f47b3a788109ddb3f88f1181792424fa332123a25ea8", "037495e920a1f032916193aa80ea97a4c3a611dec9ab47ccc969deb664f5f88bbe"], - "proof_of_knowledge": "6689a8d414eb4961308e21f8caa1045236efded4f3de9209dc07547e88be3b42e192de9bed27fb78a7a4d4e35a0422f11f52631b8e66d69e609398eaff2770b8", + "proof_of_knowledge": "191a4ef1851286e2fd6cebd483385452cbb12f43386241854939252c4ed8846b8631f9e69be37ccbd3a0b4593a8f63738747e165a22d0b5786eeb74e59a17837", "signing_shares": { "2": "1dd3cb3e2370e6af22917415f0ad584514807b58b3cc40d2230a26e115f02771", "3": "dd25ee86acd01f996618aa0d1153f5e8fbc929a8e8a18b8f0a15f91d087217e2" @@ -27,7 +27,12 @@ "coefficient": "f7ba0cbbffbea8aaceb3ade54bdbf35bafb1cda15b65ad490e0c63dd069a7c9f", "vss_commitments": ["03ef10370a008cd95e179dc51e2cb7828f30b72d254e5166484f927c84ab326582", "022ce0dac0db217ba326fbbe3e6132d45e2a4bfa0a0c3790d91eacce9a1c2d6a10"], "proof_of_knowledge": "a319dd51cf64b3896c22f54154812d4ae76cfa95f46f53ef69241fd702456fef32da76cc93d3a541ca495b723e793ee90c32440da5f314e2e58a2dc30550314a", - "verifying_share": "029ecb3a4db28a82e7b8d600d42711b02790dde3f063f0ecec6f812c1c5d7dcefc" + "signing_shares": { + "1": "b489a711942526abbb5330a8215d2e740f7dbddec3452006993a8cea3ac278cb", + "3": "20255dc07b1fb78bdf90bd85fd2389c988c8250faee11826656a09142fa9fc97" + }, + "verifying_share": "029ecb3a4db28a82e7b8d600d42711b02790dde3f063f0ecec6f812c1c5d7dcefc", + "signing_share": "9131f1241cd8f95f6439b0f5edc2ecb969a2d3db9c85fe5added77116d41d0ce" }, "3": { "identifier": 3, @@ -35,7 +40,12 @@ "coefficient": "42ff6f39ce4f97f279781378ebcf93df47add84d75882cd31b266e83f76e25f6", "vss_commitments": ["02da186c3863c5600b471a2799cb6f15ae4d8315a2f225c177798880e75ac820a0", "03e6a36e7fa4b117c1aa428886672e3a35d926bb4c585a9b07d8ee9a3387420067"], "proof_of_knowledge": "6e115d9e63fd15d432b380ccf1ec4ed03340fcf96caeae8985aedb5f905b1a65dc422ffe5878988fbbc55454857736c7755d9c8f5ee6822c8833ea21d54dba36", - "verifying_share": "02c98b3c2e9f4bde4cf90dc9c7be639e5adda6ea09fc605239880a22cb836f7145" + "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 index 176aad6..682db37 100644 --- a/frost-secp256k1-tr/tests/integration_tests.rs +++ b/frost-secp256k1-tr/tests/integration_tests.rs @@ -1,6 +1,5 @@ use frost_secp256k1_tr::*; 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< Secp256K1Sha256TR, @@ -83,7 +82,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< Secp256K1Sha256TR, @@ -91,113 +90,50 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256TR, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256TR, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256TR, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256TR, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< Secp256K1Sha256TR, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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 = 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; @@ -211,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; @@ -225,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; @@ -241,13 +177,13 @@ fn check_sign_with_dealer_fails_with_invalid_max_signers() { /// value is working. #[test] fn check_share_generation_secp256k1_tr_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; @@ -261,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; @@ -275,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; @@ -339,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::< Secp256K1Sha256TR, @@ -349,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::< Secp256K1Sha256TR, _, @@ -358,9 +294,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::< 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 index b2e3f9a..3cf68f2 100644 --- a/frost-secp256k1-tr/tests/interoperability_tests.rs +++ b/frost-secp256k1-tr/tests/interoperability_tests.rs @@ -1,25 +1,24 @@ use frost_secp256k1_tr::*; use crate::Secp256K1Sha256TR; -use rand::thread_rng; mod helpers; #[test] fn check_interoperability_in_regular_sign() { - let mut rng = thread_rng(); + 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(&mut rng, b"message"); + 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 = 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 @@ -27,7 +26,7 @@ fn check_interoperability_in_sign_with_dkg() { for _ in 0..32 { let (message, group_signature, group_pubkey) = frost_core::tests::ciphersuite_generic::check_sign_with_dkg::( - rng.clone(), + rng, ); helpers::verify_signature(&message, &group_signature, &group_pubkey); @@ -36,14 +35,14 @@ 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 (message, group_signature, group_pubkey) = frost_core::tests::ciphersuite_generic::check_sign_with_dealer::( - rng.clone(), + rng, ); // Check that the threshold signature can be verified by the `ed25519_dalek` crate diff --git a/frost-secp256k1-tr/tests/recreation_tests.rs b/frost-secp256k1-tr/tests/recreation_tests.rs index f6f71ea..56cd8f4 100644 --- a/frost-secp256k1-tr/tests/recreation_tests.rs +++ b/frost-secp256k1-tr/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-secp256k1-tr/tests/rerandomized_tests.rs b/frost-secp256k1-tr/tests/rerandomized_tests.rs index 67e1431..7b4144c 100644 --- a/frost-secp256k1-tr/tests/rerandomized_tests.rs +++ b/frost-secp256k1-tr/tests/rerandomized_tests.rs @@ -1,9 +1,8 @@ use frost_secp256k1_tr::Secp256K1Sha256TR; -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-tr/tests/serde_tests.rs b/frost-secp256k1-tr/tests/serde_tests.rs index 62a70e7..e13cf1d 100644 --- a/frost-secp256k1-tr/tests/serde_tests.rs +++ b/frost-secp256k1-tr/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-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()); diff --git a/frost-secp256k1-tr/tests/serialization_tests.rs b/frost-secp256k1-tr/tests/serialization_tests.rs index d38bc5e..9975017 100644 --- a/frost-secp256k1-tr/tests/serialization_tests.rs +++ b/frost-secp256k1-tr/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..b147c34 --- /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/tweaking_tests.rs b/frost-secp256k1-tr/tests/tweaking_tests.rs index 3fc74ae..dddbed1 100644 --- a/frost-secp256k1-tr/tests/tweaking_tests.rs +++ b/frost-secp256k1-tr/tests/tweaking_tests.rs @@ -12,19 +12,19 @@ mod helpers; #[test] fn check_tweaked_sign_with_dealer() -> Result<(), Box> { use frost_secp256k1_tr as frost; - use rand::thread_rng; + use std::collections::BTreeMap; let merkle_root: Vec = vec![12; 32]; - 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( max_signers, min_signers, frost::keys::IdentifierList::Default, - &mut rng, + rng, )?; let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); for (identifier, secret_share) in shares { diff --git a/frost-secp256k1/CHANGELOG.md b/frost-secp256k1/CHANGELOG.md new file mode 100644 index 0000000..b1dedcb --- /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 4a6ac8e..8d94271 100644 --- a/frost-secp256k1/Cargo.toml +++ b/frost-secp256k1/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "frost-secp256k1" 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", "threshold", "signature"] +keywords = ["cryptography", "crypto", "secp256k1", "threshold", "signature"] description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST." [package.metadata.docs.rs] @@ -16,16 +17,16 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features.workspace = true -frost-core = { path = "../frost-core", version = "2.1.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", default-features = false } +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 = { path = "../frost-core", version = "2.1.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.1.0", features = ["test-impl"] } +frost-core = { workspace = true, features = ["test-impl"] } +frost-rerandomized = { workspace = true, features = ["test-impl"] } insta.workspace = true hex.workspace = true lazy_static.workspace = true @@ -33,21 +34,17 @@ proptest.workspace = true rand.workspace = true rand_chacha.workspace = true serde_json.workspace = true +tokio.workspace = true [features] -nightly = [] -default = ["serialization", "cheater-detection", "std"] +default = ["serialization"] #! ## Features -## Enable standard library support. -std = ["frost-core/std"] ## 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"] -## Enable cheater detection -cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] [lib] # Disables non-criterion benchmark which is not used; prevents errors diff --git a/frost-secp256k1/README.md b/frost-secp256k1/README.md index 9e4928b..c65e578 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( diff --git a/frost-secp256k1/benches/bench.rs b/frost-secp256k1/benches/bench.rs index c577363..cd89e8e 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 5d62857..48d7681 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 a 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/refresh.rs b/frost-secp256k1/src/keys/refresh.rs index c270fc2..bb4b6dd 100644 --- a/frost-secp256k1/src/keys/refresh.rs +++ b/frost-secp256k1/src/keys/refresh.rs @@ -1,35 +1,64 @@ //! Refresh Shares //! -//! Implements the functionality to refresh a share. This requires the participation -//! of all the remaining signers. This can be done using a Trusted Dealer or -//! DKG (not yet implemented) +//! Refer to [`frost_core::keys::refresh`] for more details. -use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; -use alloc::vec::Vec; +use crate::{ + frost, + keys::dkg::{round1, round2}, + CryptoRng, Error, Identifier, RngCore, +}; +use alloc::{collections::btree_map::BTreeMap, vec::Vec}; use super::{KeyPackage, PublicKeyPackage, SecretShare}; -/// Refreshes shares using a trusted dealer -pub fn compute_refreshing_shares( +/// Refer to [`frost_core::keys::refresh::compute_refreshing_shares`]. +pub fn compute_refreshing_shares( old_pub_key_package: PublicKeyPackage, - max_signers: u16, - min_signers: u16, identifiers: &[Identifier], mut rng: &mut R, ) -> Result<(Vec, PublicKeyPackage), Error> { - frost::keys::refresh::compute_refreshing_shares( - old_pub_key_package, - max_signers, - min_signers, - identifiers, - &mut rng, - ) + frost::keys::refresh::compute_refreshing_shares(old_pub_key_package, identifiers, &mut rng) } -/// Each participant refreshed their shares -pub fn refresh_share( +/// 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 88bce01..48b1506 100644 --- a/frost-secp256k1/src/keys/repairable.rs +++ b/frost-secp256k1/src/keys/repairable.rs @@ -6,59 +6,65 @@ 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 eec2c8e..bebcd1f 100644 --- a/frost-secp256k1/src/lib.rs +++ b/frost-secp256k1/src/lib.rs @@ -1,7 +1,6 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![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!()] @@ -154,7 +153,7 @@ 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 } @@ -429,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 0000000..7f7fc93 --- /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 b87d22a..d3b1c68 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 d1b6c22..7be35ea 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/vss_commitment.rs b/frost-secp256k1/src/tests/vss_commitment.rs index 1a09195..79aa89a 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 6048b48..e9f788e 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.rs b/frost-secp256k1/tests/helpers/samples.rs index a69632d..d9cbf5b 100644 --- a/frost-secp256k1/tests/helpers/samples.rs +++ b/frost-secp256k1/tests/helpers/samples.rs @@ -104,7 +104,19 @@ pub fn public_key_package() -> PublicKeyPackage { 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. diff --git a/frost-secp256k1/tests/integration_tests.rs b/frost-secp256k1/tests/integration_tests.rs index d098f26..343214e 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,21 @@ 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 = thread_rng(); + 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 = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::( rng, @@ -82,7 +81,7 @@ fn check_refresh_shares_with_dealer_serialisation() { #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { - let rng = thread_rng(); + let rng = rand::rngs::OsRng; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< Secp256K1Sha256, @@ -90,113 +89,49 @@ fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { >(rng); } -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 1; - let max_signers = 4; - let error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 3; - let max_signers = 3; - let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { - let rng = thread_rng(); - let identifiers = vec![ - Identifier::try_from(1).unwrap(), - Identifier::try_from(3).unwrap(), - Identifier::try_from(4).unwrap(), - Identifier::try_from(5).unwrap(), - ]; - let min_signers = 6; - let max_signers = 4; - let error: frost_core::Error = Error::InvalidMinSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - -#[test] -fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { - let rng = thread_rng(); - let identifiers = vec![Identifier::try_from(1).unwrap()]; - let min_signers = 3; - let max_signers = 1; - let error = Error::InvalidMaxSigners; - - frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< - Secp256K1Sha256, - _, - >(max_signers, min_signers, &identifiers, error, rng); -} - #[test] fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { - let rng = thread_rng(); + 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 min_signers = 2; - let max_signers = 4; let error = Error::UnknownIdentifier; frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< Secp256K1Sha256, _, - >(max_signers, min_signers, &identifiers, error, rng); + >(&identifiers, error, rng); } #[test] fn check_refresh_shares_with_dkg() { - let rng = thread_rng(); + 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; @@ -210,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; @@ -224,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; @@ -240,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; @@ -260,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; @@ -274,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; @@ -338,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, @@ -348,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, ); @@ -356,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 e61b303..1e8eb20 100644 --- a/frost-secp256k1/tests/recreation_tests.rs +++ b/frost-secp256k1/tests/recreation_tests.rs @@ -101,8 +101,25 @@ 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); } diff --git a/frost-secp256k1/tests/rerandomized_tests.rs b/frost-secp256k1/tests/rerandomized_tests.rs index 65cf0df..c7845f1 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 82a0735..5b02fda 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 ec8d6f8..08e53d3 100644 --- a/frost-secp256k1/tests/serialization_tests.rs +++ b/frost-secp256k1/tests/serialization_tests.rs @@ -82,6 +82,17 @@ 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(); 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 0000000..0435fda --- /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/gencode/Cargo.toml b/gencode/Cargo.toml index 8487d9e..44dead9 100644 --- a/gencode/Cargo.toml +++ b/gencode/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" 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.workspace = true diff --git a/gencode/src/main.rs b/gencode/src/main.rs index 8a420fc..9dee2d5 100644 --- a/gencode/src/main.rs +++ b/gencode/src/main.rs @@ -167,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()) @@ -181,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() } @@ -212,6 +219,7 @@ fn main() -> ExitCode { "ristretto255_sha512", "ristretto255", "", + "", ] .iter() .map(|x| x.to_string()) @@ -252,6 +260,7 @@ fn main() -> ExitCode { "p256_sha256", "p256", "

", + "", ], ), ( @@ -265,6 +274,7 @@ fn main() -> ExitCode { "ed25519_sha512", "ed25519", "", + "", ], ), ( @@ -278,6 +288,7 @@ fn main() -> ExitCode { "ed448_shake256", "ed448", "", + "", ], ), ( @@ -291,6 +302,7 @@ 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*", ], ), ( @@ -304,6 +316,7 @@ fn main() -> ExitCode { "secp256k1_tr_sha256", "secp256k1_tr", "", + "", ], ), ] { @@ -341,7 +354,9 @@ 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", diff --git a/performance.md b/performance.md index 6b21dd4..029632d 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