diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 39e5030..db9f98a 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -23,7 +23,7 @@ jobs: RUST_BACKTRACE: full steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -38,10 +38,10 @@ jobs: run: cargo install cargo-llvm-cov - name: Run tests - run: cargo llvm-cov --lcov --no-report --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' + run: cargo llvm-cov --lcov --no-report --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs|interoperability_tests.rs' - name: Generate coverage report - run: cargo llvm-cov report --lcov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' --output-path lcov.info + run: cargo llvm-cov report --lcov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs|interoperability_tests.rs' --output-path lcov.info - name: Upload coverage report to Codecov uses: codecov/codecov-action@v4.5.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bed5b04..aebf2fa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.2 with: persist-credentials: false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 97b3f2d..a8088ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - uses: actions-rs/toolchain@v1.0.7 with: toolchain: beta @@ -29,9 +29,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 - # Re-resolve Cargo.lock with minimal versions - - uses: dtolnay/rust-toolchain@nightly + - uses: actions/checkout@v4.2.2 + # Re-resolve Cargo.lock with minimal versions. + # This only works with nightly. We pin to a specific version because + # newer versions use lock file version 4, but the MSRV cargo does not + # support that. + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2024-09-20 - run: cargo update -Z minimal-versions # Now check that `cargo build` works with respect to the oldest possible # deps and the stated MSRV @@ -46,7 +51,7 @@ jobs: # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@v4.1.7 + # - uses: actions/checkout@v4.2.2 # - uses: dtolnay/rust-toolchain@stable # - run: cargo install cargo-all-features # # We check and then test because some test dependencies could help @@ -64,9 +69,9 @@ jobs: # Skip ed448 which does not support it. strategy: matrix: - crate: [ristretto255, ed25519, p256, secp256k1, rerandomized] + crate: [ristretto255, ed25519, p256, secp256k1, secp256k1-tr, rerandomized] steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -79,7 +84,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - uses: actions-rs/toolchain@v1.0.7 with: toolchain: beta @@ -94,7 +99,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -129,7 +134,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -151,7 +156,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -173,7 +178,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -192,8 +197,8 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4.1.7 - - uses: reviewdog/action-actionlint@v1.54.0 + - uses: actions/checkout@v4.2.2 + - uses: reviewdog/action-actionlint@v1.57.0 with: level: warning fail_on_error: false diff --git a/Cargo.toml b/Cargo.toml index cbf054f..658adb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,10 @@ members = [ "frost-p256", "frost-ristretto255", "frost-secp256k1", + "frost-secp256k1-tr", "frost-rerandomized", "gencode" ] + +[profile.test.package."*"] +opt-level = 3 \ No newline at end of file diff --git a/README.md b/README.md index 8f22461..019b60d 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,16 @@ [![CI](https://github.com/ZcashFoundation/frost/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/ZcashFoundation/frost/actions/workflows/main.yml) -| Crate | | Crates.io | Documentation | -| ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| Generic FROST implementation | [`frost-core`] | [![crates.io](https://img.shields.io/crates/v/frost-core.svg)](https://crates.io/crates/frost-core) | [![Documentation](https://docs.rs/frost-core/badge.svg)](https://docs.rs/frost-core) | -| Ristretto255 ciphersuite | [`frost-ristretto255`] | [![crates.io](https://img.shields.io/crates/v/frost-ristretto255.svg)](https://crates.io/crates/frost-ristretto255) | [![Documentation](https://docs.rs/frost-ristretto255/badge.svg)](https://docs.rs/frost-ristretto255) | -| Ed25519 ciphersuite | [`frost-ed25519`] | [![crates.io](https://img.shields.io/crates/v/frost-ed25519.svg)](https://crates.io/crates/frost-ed25519) | [![Documentation](https://docs.rs/frost-ed25519/badge.svg)](https://docs.rs/frost-ed25519) | -| Ed448 ciphersuite | [`frost-ed448`] | [![crates.io](https://img.shields.io/crates/v/frost-ed448.svg)](https://crates.io/crates/frost-ed448) | [![Documentation](https://docs.rs/frost-ed448/badge.svg)](https://docs.rs/frost-ed448) | -| P-256 ciphersuite | [`frost-p256`] | [![crates.io](https://img.shields.io/crates/v/frost-p256.svg)](https://crates.io/crates/frost-p256) | [![Documentation](https://docs.rs/frost-p256/badge.svg)](https://docs.rs/frost-p256) | -| secp256k1 ciphersuite | [`frost-secp256k1`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1.svg)](https://crates.io/crates/frost-secp256k1) | [![Documentation](https://docs.rs/frost-secp256k1/badge.svg)](https://docs.rs/frost-secp256k1) | -| Generic Re-randomized FROST | [`frost-rerandomized`] | [![crates.io](https://img.shields.io/crates/v/frost-rerandomized.svg)](https://crates.io/crates/frost-rerandomized) | [![Documentation](https://docs.rs/frost-rerandomized/badge.svg)](https://docs.rs/frost-rerandomized) | +| Crate | | Crates.io | Documentation | +| ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Generic FROST implementation | [`frost-core`] | [![crates.io](https://img.shields.io/crates/v/frost-core.svg)](https://crates.io/crates/frost-core) | [![Documentation](https://docs.rs/frost-core/badge.svg)](https://docs.rs/frost-core) | +| Ristretto255 ciphersuite | [`frost-ristretto255`] | [![crates.io](https://img.shields.io/crates/v/frost-ristretto255.svg)](https://crates.io/crates/frost-ristretto255) | [![Documentation](https://docs.rs/frost-ristretto255/badge.svg)](https://docs.rs/frost-ristretto255) | +| Ed25519 ciphersuite | [`frost-ed25519`] | [![crates.io](https://img.shields.io/crates/v/frost-ed25519.svg)](https://crates.io/crates/frost-ed25519) | [![Documentation](https://docs.rs/frost-ed25519/badge.svg)](https://docs.rs/frost-ed25519) | +| Ed448 ciphersuite | [`frost-ed448`] | [![crates.io](https://img.shields.io/crates/v/frost-ed448.svg)](https://crates.io/crates/frost-ed448) | [![Documentation](https://docs.rs/frost-ed448/badge.svg)](https://docs.rs/frost-ed448) | +| P-256 ciphersuite | [`frost-p256`] | [![crates.io](https://img.shields.io/crates/v/frost-p256.svg)](https://crates.io/crates/frost-p256) | [![Documentation](https://docs.rs/frost-p256/badge.svg)](https://docs.rs/frost-p256) | +| secp256k1 ciphersuite | [`frost-secp256k1`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1.svg)](https://crates.io/crates/frost-secp256k1) | [![Documentation](https://docs.rs/frost-secp256k1/badge.svg)](https://docs.rs/frost-secp256k1) | +| secp256k1 ciphersuite (Taproot) | [`frost-secp256k1-tr`] | [![crates.io](https://img.shields.io/crates/v/frost-secp256k1-tr.svg)](https://crates.io/crates/frost-secp256k1-tr) | [![Documentation](https://docs.rs/frost-secp256k1-tr/badge.svg)](https://docs.rs/frost-secp256k1-tr) | +| Generic Re-randomized FROST | [`frost-rerandomized`] | [![crates.io](https://img.shields.io/crates/v/frost-rerandomized.svg)](https://crates.io/crates/frost-rerandomized) | [![Documentation](https://docs.rs/frost-rerandomized/badge.svg)](https://docs.rs/frost-rerandomized) | Rust implementations of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/). @@ -59,7 +60,7 @@ of the v0.6.0 release (corresponding to commit 5fa17ed) of the following crates: - frost-ristretto255 This includes key generation (both trusted dealer and DKG) and FROST signing. -This does not include rerandomized FROST. +This does not include frost-secp256k1-tr and rerandomized FROST. The parts of the [`Ed448-Goldilocks`](https://github.com/crate-crypto/Ed448-Goldilocks) diff --git a/book/src/dev/frost-dependencies-for-audit.md b/book/src/dev/frost-dependencies-for-audit.md index 95227aa..cc8c13c 100644 --- a/book/src/dev/frost-dependencies-for-audit.md +++ b/book/src/dev/frost-dependencies-for-audit.md @@ -46,6 +46,7 @@ The following crates and dependencies are out of scope for the audit. | Name | Version | Notes |------| ------- | ----- | frost-rerandomized | v0.2.0 | To be audited after the security proof is complete. +| frost-secp256k1-tr | N/A | frost-secp256k1 with Taproot support, has not been audited yet. ### `frost-core` Dependencies diff --git a/book/src/dev/release-checklist.md b/book/src/dev/release-checklist.md index d4ba809..84e59e0 100644 --- a/book/src/dev/release-checklist.md +++ b/book/src/dev/release-checklist.md @@ -23,6 +23,7 @@ 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) 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. @@ -74,7 +75,7 @@ 20. Publish it with `cargo publish -p frost-rerandomized` -21. Check if other crates are ready to be published: `for cs in ristretto255 ed25519 secp256k1 p256 ed448; do cargo publish -p frost-$cs --dry-run; done`. Fix any issues if needed. +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. 1. If you get an error like this: @@ -86,12 +87,12 @@ 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” -23. Publish those crates: `for cs in ristretto255 ed25519 secp256k1 p256 ed448; do cargo publish -p frost-$cs; done` +23. 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:  +24. 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) @@ -99,7 +100,8 @@ 4. [Frost p256](https://crates.io/crates/frost-p256/versions) 5. [Frost ristretto255](https://crates.io/crates/frost-ristretto255/versions) 6. [Frost secp256k1](https://crates.io/crates/frost-secp256k1/versions) - 7. [Frost rerandomized](https://crates.io/crates/frost-rerandomized/versions) + 7. [Frost secp256k1 tr](https://crates.io/crates/frost-secp256k1-tr/versions) + 8. [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 diff --git a/book/src/frost.md b/book/src/frost.md index 1daa881..3f9b003 100644 --- a/book/src/frost.md +++ b/book/src/frost.md @@ -78,12 +78,12 @@ be able to produce the final signature. Of course, the Coordinator is still free to start the process with only 2 participants if they wish. ``` -## Verifying +## Verifying Signatures Signature verification is carried out as normal with single-party signatures, along with the signed message and the group verifying key as inputs. -## Repairing +## Repairing Shares Repairing shares allow participants to help another participant recover their share if they have lost it, or also issue a new share to a new participant @@ -97,7 +97,7 @@ The functionality works in such a way that each participant running the repair share function is not able to obtain the share that is being recovered or issued. -## Refreshing +## Refreshing Shares Refreshing shares allow participants (or a subset of them) to update their shares in a way that maintains the same group public key. Some applications are: @@ -117,6 +117,21 @@ This is also possible via Distributed Key Generation but this has not yet been implemented. ``` +```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 a unnaceptable risk to your use case, +you will need to migrate to a new group if that makes sense to your application. +``` + ## Ciphersuites FROST is a generic protocol that works with any adequate prime-order group, diff --git a/book/src/tutorial.md b/book/src/tutorial.md index 0f80f49..15df64c 100644 --- a/book/src/tutorial.md +++ b/book/src/tutorial.md @@ -5,8 +5,8 @@ a generic implementation of the protocol, which can't be used directly without a concrete instantiation. The ciphersuite crates (`frost-ristretto255`, `frost-ed25519`, `frost-ed448`, -`frost-p256`, and `frost-secp256k1`) provide ciphersuites to use with -`frost-core`, but also re-expose the `frost-core` functions without +`frost-p256`, `frost-secp256k1` and `frost-secp256k1-tr`) provide ciphersuites +to use with `frost-core`, but also re-expose the `frost-core` functions without generics. If you will only use a single ciphersuite, then we recommend using those functions, and this tutorial will follow this approach. If you need to support multiple ciphersuites then feel free to use diff --git a/book/src/tutorial/importing.md b/book/src/tutorial/importing.md index 68d1225..ec909a6 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.0.0-rc.0" +frost-ristretto255 = "2.0.0" ``` ## Handling errors @@ -31,14 +31,14 @@ Format](../user/serialization.md). ### serde -Alternatively, if you would like to user another format such as JSON, you can +Alternatively, if you would like to use another format such as JSON, you can enable the `serde` feature (which is *not* enabled by default). When it is enabled, you can use [serde](https://serde.rs/) to serialize any structure that needs to be transmitted. The importing would look like: ``` [dependencies] -frost-ristretto255 = { version = "2.0.0-rc.0", features = ["serde"] } +frost-ristretto255 = { version = "2.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 cf75b3d..5a65f1a 100644 --- a/book/src/tutorial/refreshing-shares.md +++ b/book/src/tutorial/refreshing-shares.md @@ -32,3 +32,9 @@ Applications should first ensure that all participants who refreshed their `KeyPackages`. How this is done is up to the application; it might require sucessfully 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 to the [Understanding +FROST](../frost.md#refreshing-shares) section. +``` \ No newline at end of file diff --git a/book/src/user.md b/book/src/user.md index 30c1cc8..f0c4635 100644 --- a/book/src/user.md +++ b/book/src/user.md @@ -7,3 +7,4 @@ - [frost-p256](https://docs.rs/frost-p256/) - [frost-ristretto255](https://docs.rs/frost-ristretto255/) - [frost-secp256k1](https://docs.rs/frost-secp256k1/) +- [frost-secp256k1-tr](https://docs.rs/frost-secp256k1-tr/) diff --git a/book/src/user/frost-secp256k1-tr.md b/book/src/user/frost-secp256k1-tr.md new file mode 100644 index 0000000..7334064 --- /dev/null +++ b/book/src/user/frost-secp256k1-tr.md @@ -0,0 +1 @@ +{{#include ../../../frost-secp256k1-tr/README.md}} \ No newline at end of file diff --git a/book/src/user/frost-secp256k1-tr/dkg.md b/book/src/user/frost-secp256k1-tr/dkg.md new file mode 100644 index 0000000..b93feef --- /dev/null +++ b/book/src/user/frost-secp256k1-tr/dkg.md @@ -0,0 +1 @@ +{{#include ../../../../frost-secp256k1-tr/dkg.md}} \ No newline at end of file diff --git a/frost-core/CHANGELOG.md b/frost-core/CHANGELOG.md index 915ef47..be9108b 100644 --- a/frost-core/CHANGELOG.md +++ b/frost-core/CHANGELOG.md @@ -4,12 +4,28 @@ Entries are listed in reverse chronological order. ## Unreleased +* 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()`). +* Added frost-secp256k1-tr crate, allowing to generate Bitcoin Taproot (BIP340/BIP341) + compatible signatures. + +## 2.0.0 + +* Updated docs +* Added missing `derive(Getters)` for `dkg::{round1, round2}` +* Added `internal` feature for `validate_num_of_signers` * Added refresh share functionality for trusted dealer: `frost_core::keys::refresh::{compute_refreshing_shares, refresh_share}` * Added a `'static` bound to the `Ciphersuite` trait. This is a breaking change, but it's likely to not require any code changes since most ciphersuite implementations are probably just empty structs. The bound makes it possible to use `frost_core::Error` in `Box`. +* Added getters to `round1::SecretPackage` and `round2::SecretPackage`. +* Added a `frost_core::verify_signature_share()` function which allows verifying + individual signature shares. This is not required for regular FROST usage but + might useful in certain situations where it is desired to verify each + individual signature share before aggregating the signature. ## 2.0.0-rc.0 diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index cba9291..ee2271c 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -33,7 +33,7 @@ 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 = "1.0.29", default-features = false, optional = true } +thiserror = { version = "2.0.3", default-features = false, optional = true } visibility = "0.1.0" zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } itertools = { version = "0.13.0", default-features = false } diff --git a/frost-core/src/batch.rs b/frost-core/src/batch.rs index 3aa1e80..a30a109 100644 --- a/frost-core/src/batch.rs +++ b/frost-core/src/batch.rs @@ -33,10 +33,14 @@ where where M: AsRef<[u8]>, { - // Compute c now to avoid dependency on the msg lifetime. - let c = crate::challenge(&sig.R, &vk, msg.as_ref())?; - - Ok(Self { vk, sig, c }) + let (msg, sig, vk) = ::pre_verify(msg.as_ref(), &sig, &vk)?; + let c = ::challenge(&sig.R, &vk, &msg)?; + + Ok(Self { + vk: *vk, + sig: *sig, + c, + }) } } diff --git a/frost-core/src/error.rs b/frost-core/src/error.rs index 6fdd360..78662df 100644 --- a/frost-core/src/error.rs +++ b/frost-core/src/error.rs @@ -74,7 +74,11 @@ pub enum Error { }, /// Secret share verification failed. #[error("Invalid secret share.")] - InvalidSecretShare, + InvalidSecretShare { + /// The identifier of the signer whose secret share validation failed, + /// if possible to identify. + culprit: Option>, + }, /// Round 1 package not found for Round 2 participant. #[error("Round 1 package not found for Round 2 participant.")] PackageNotFound, @@ -132,8 +136,10 @@ where | Error::InvalidProofOfKnowledge { culprit: identifier, } => Some(*identifier), - Error::InvalidSecretShare - | Error::InvalidMinSigners + Error::InvalidSecretShare { + culprit: identifier, + } => *identifier, + Error::InvalidMinSigners | Error::InvalidMaxSigners | Error::InvalidCoefficients | Error::MalformedIdentifier diff --git a/frost-core/src/keys.rs b/frost-core/src/keys.rs index 5a884e4..9834665 100644 --- a/frost-core/src/keys.rs +++ b/frost-core/src/keys.rs @@ -327,8 +327,8 @@ where .collect::>>() } - /// Returns VerifiableSecretSharingCommitment from a iterator of serialized - /// CoefficientCommitments (e.g. a Vec>). + /// Returns VerifiableSecretSharingCommitment from an iterator of serialized + /// CoefficientCommitments (e.g. a [`Vec>`]). pub fn deserialize(serialized_coefficient_commitments: I) -> Result> where I: IntoIterator, @@ -423,7 +423,18 @@ where let result = evaluate_vss(self.identifier, &self.commitment); if !(f_result == result) { - return Err(Error::InvalidSecretShare); + // The culprit needs to be identified by the caller if needed, + // because this function is called in two different contexts: + // - after trusted dealer key generation, by the participant who + // receives the SecretShare. In that case it does not make sense + // to identify themselves as the culprit, since the issue was with + // the Coordinator or in the communication. + // - during DKG, where a "fake" SecretShare is built just to reuse + // the verification logic and it does make sense to identify the + // culprit. Note that in this case, self.identifier is the caller's + // identifier and not the culprit's, so we couldn't identify + // the culprit inside this function anyway. + return Err(Error::InvalidSecretShare { culprit: None }); } Ok(( @@ -760,6 +771,8 @@ where } } +/// Validates the number of signers. +#[cfg_attr(feature = "internals", visibility::make(pub))] fn validate_num_of_signers( min_signers: u16, max_signers: u16, diff --git a/frost-core/src/keys/dkg.rs b/frost-core/src/keys/dkg.rs index aa3a56a..dcdb9b6 100644 --- a/frost-core/src/keys/dkg.rs +++ b/frost-core/src/keys/dkg.rs @@ -116,12 +116,13 @@ pub mod round1 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq, Getters)] pub struct SecretPackage { /// The identifier of the participant holding the secret. 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) pub(crate) commitment: VerifiableSecretSharingCommitment, @@ -233,7 +234,7 @@ pub mod round2 { /// # Security /// /// This package MUST NOT be sent to other participants! - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq, Getters)] pub struct SecretPackage { /// The identifier of the participant holding the secret. pub(crate) identifier: Identifier, @@ -354,8 +355,7 @@ pub(crate) fn compute_proof_of_knowledge // > a_{i0} by calculating σ_i = (R_i, μ_i), such that k ← Z_q, R_i = g^k, // > c_i = H(i, Φ, g^{a_{i0}} , R_i), μ_i = k + a_{i0} · c_i, with Φ being // > a context string to prevent replay attacks. - let k = <::Field>::random(&mut rng); - let R_i = ::generator() * k; + let (k, R_i) = ::generate_nonce(&mut rng); let c_i = challenge::(identifier, &commitment.verifying_key()?, &R_i)?; let a_i0 = *coefficients .first() @@ -524,7 +524,16 @@ pub fn part3( }; // Verify the share. We don't need the result. - let _ = secret_share.verify()?; + // Identify the culprit if an InvalidSecretShare error is returned. + let _ = secret_share.verify().map_err(|e| { + if let Error::InvalidSecretShare { .. } = e { + Error::InvalidSecretShare { + culprit: Some(*sender_identifier), + } + } else { + e + } + })?; // Round 2, Step 3 // @@ -560,5 +569,5 @@ pub fn part3( min_signers: round2_secret_package.min_signers, }; - Ok((key_package, public_key_package)) + C::post_dkg(key_package, public_key_package) } diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index fbecfe0..7d78e2d 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -25,6 +25,7 @@ use alloc::{ use derive_getters::Getters; #[cfg(any(test, feature = "test-impl"))] use hex::FromHex; +use keys::PublicKeyPackage; use rand_core::{CryptoRng, RngCore}; use serialization::SerializableScalar; use zeroize::Zeroize; @@ -64,11 +65,7 @@ pub use verifying_key::VerifyingKey; /// /// [challenge]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa #[derive(Copy, Clone)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct Challenge( - pub(crate) <::Field as Field>::Scalar, -); +pub struct Challenge(pub(crate) <::Field as Field>::Scalar); impl Challenge where @@ -138,6 +135,8 @@ where /// Generates a random nonzero scalar. /// /// It assumes that the Scalar Eq/PartialEq implementation is constant-time. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn random_nonzero(rng: &mut R) -> Scalar { loop { let scalar = <::Field>::random(rng); @@ -192,9 +191,7 @@ where /// /// #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct BindingFactor(Scalar); +pub struct BindingFactor(Scalar); impl BindingFactor where @@ -469,19 +466,24 @@ where /// The product of all signers' individual commitments, published as part of the /// final signature. #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct GroupCommitment(pub(crate) Element); +pub struct GroupCommitment(pub(crate) Element); impl GroupCommitment where C: Ciphersuite, { /// Return the underlying element. - #[cfg(feature = "internals")] - pub fn to_element(self) -> ::Element { + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_element(self) -> ::Element { self.0 } + + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn from_element(element: Element) -> Self { + Self(element) + } } /// Generates the group commitment which is published as part of the joint @@ -583,12 +585,15 @@ where return Err(Error::UnknownIdentifier); } + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, signature_shares, pubkeys)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &pubkeys.verifying_key, &[])?; + compute_binding_factor_list(&signing_package, &pubkeys.verifying_key, &[])?; // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // The aggregation of the signature shares by summing them up, resulting in // a plain Schnorr signature. @@ -618,10 +623,10 @@ where #[cfg(feature = "cheater-detection")] if verification_result.is_err() { detect_cheater( - group_commitment, - pubkeys, - signing_package, - signature_shares, + &group_commitment, + &pubkeys, + &signing_package, + &signature_shares, &binding_factor_list, )?; } @@ -634,52 +639,138 @@ where /// Optional cheater detection feature /// Each share is verified to find the cheater +#[cfg(feature = "cheater-detection")] fn detect_cheater( - group_commitment: GroupCommitment, + group_commitment: &GroupCommitment, pubkeys: &keys::PublicKeyPackage, signing_package: &SigningPackage, signature_shares: &BTreeMap, round2::SignatureShare>, binding_factor_list: &BindingFactorList, ) -> Result<(), Error> { // Compute the per-message challenge. - let challenge = crate::challenge::( + let challenge = ::challenge( &group_commitment.0, &pubkeys.verifying_key, - signing_package.message().as_slice(), + signing_package.message(), )?; // Verify the signature shares. - for (signature_share_identifier, signature_share) in signature_shares { + for (identifier, signature_share) in signature_shares { // Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_, // and where s[i] is a secret share of the constant term of _f_, the secret polynomial. - let signer_pubkey = pubkeys + let verifying_share = pubkeys .verifying_shares - .get(signature_share_identifier) + .get(identifier) .ok_or(Error::UnknownIdentifier)?; - // Compute Lagrange coefficient. - let lambda_i = derive_interpolating_value(signature_share_identifier, signing_package)?; - - let binding_factor = binding_factor_list - .get(signature_share_identifier) - .ok_or(Error::UnknownIdentifier)?; - - // Compute the commitment share. - let R_share = signing_package - .signing_commitment(signature_share_identifier) - .ok_or(Error::UnknownIdentifier)? - .to_group_commitment_share(binding_factor); - - // Compute relation values to verify this signature share. - signature_share.verify( - *signature_share_identifier, - &R_share, - signer_pubkey, - lambda_i, - &challenge, + verify_signature_share_precomputed( + *identifier, + signing_package, + binding_factor_list, + group_commitment, + signature_share, + verifying_share, + challenge, )?; } // We should never reach here; but we return an error to be safe. Err(Error::InvalidSignature) } + +/// Verify a signature share for the given participant `identifier`, +/// `verifying_share` and `signature_share`; with the `signing_package` +/// for which the signature share was produced and with the group's +/// `verifying_key`. +/// +/// This is not required for regular FROST usage but might useful in certain +/// situations where it is desired to verify each individual signature share +/// before aggregating the signature. +pub fn verify_signature_share( + identifier: Identifier, + verifying_share: &keys::VerifyingShare, + signature_share: &round2::SignatureShare, + signing_package: &SigningPackage, + verifying_key: &VerifyingKey, +) -> Result<(), Error> { + // In order to reuse `pre_aggregate()`, we need to create some "dummy" containers + let signature_shares = BTreeMap::from([(identifier, *signature_share)]); + let verifying_shares = BTreeMap::from([(identifier, *verifying_share)]); + let public_key_package = PublicKeyPackage::new(verifying_shares, *verifying_key); + + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, &signature_shares, &public_key_package)?; + + // Extract the processed values back from the "dummy" containers + let verifying_share = pubkeys + .verifying_shares() + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + let verifying_key = pubkeys.verifying_key(); + let signature_share = signature_shares + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + + // 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, &[])?; + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; + + // Compute the per-message challenge. + let challenge = ::challenge( + &group_commitment.clone().to_element(), + verifying_key, + signing_package.message().as_slice(), + )?; + + verify_signature_share_precomputed( + identifier, + &signing_package, + &binding_factor_list, + &group_commitment, + signature_share, + verifying_share, + challenge, + ) +} + +/// Similar to [`verify_signature_share()`] but using a precomputed +/// `binding_factor_list` and `challenge`. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn verify_signature_share_precomputed( + signature_share_identifier: Identifier, + signing_package: &SigningPackage, + binding_factor_list: &BindingFactorList, + group_commitment: &GroupCommitment, + signature_share: &round2::SignatureShare, + verifying_share: &keys::VerifyingShare, + challenge: Challenge, +) -> Result<(), Error> { + let lambda_i = derive_interpolating_value(&signature_share_identifier, signing_package)?; + + let binding_factor = binding_factor_list + .get(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)?; + + let R_share = signing_package + .signing_commitment(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)? + .to_group_commitment_share(binding_factor); + + // Compute relation values to verify this signature share. + ::verify_share( + group_commitment, + signature_share, + signature_share_identifier, + &R_share, + verifying_share, + lambda_i, + &challenge, + )?; + + Ok(()) +} diff --git a/frost-core/src/round1.rs b/frost-core/src/round1.rs index 27e1c22..694043b 100644 --- a/frost-core/src/round1.rs +++ b/frost-core/src/round1.rs @@ -54,10 +54,16 @@ where Self::nonce_generate_from_random_bytes(secret, random_bytes) } + /// Create a nonce from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] fn from_scalar(scalar: <<::Group as Group>::Field as Field>::Scalar) -> Self { Self(SerializableScalar(scalar)) } + /// Convert a nonce into a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn to_scalar( self, ) -> <<::Group as Group>::Field as Field>::Scalar { @@ -358,6 +364,20 @@ where #[derive(Clone, Copy, PartialEq)] pub struct GroupCommitmentShare(pub(super) Element); +impl GroupCommitmentShare { + /// Create from an element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_element(element: Element) -> Self { + Self(element) + } + + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn to_element(self) -> Element { + self.0 + } +} + /// Encode the list of group signing commitments. /// /// Implements [`encode_group_commitment_list()`] from the spec. diff --git a/frost-core/src/round2.rs b/frost-core/src/round2.rs index 7b0fd45..3d86399 100644 --- a/frost-core/src/round2.rs +++ b/frost-core/src/round2.rs @@ -4,7 +4,7 @@ use core::fmt::{self, Debug}; use crate as frost; use crate::{ - challenge, Challenge, Ciphersuite, Error, Field, Group, {round1, *}, + Challenge, Ciphersuite, Error, Field, Group, {round1, *}, }; /// A participant's signature share, which the coordinator will aggregate with all other signer's @@ -71,7 +71,8 @@ where challenge: &Challenge, ) -> Result<(), Error> { if (::generator() * self.to_scalar()) - != (group_commitment_share.0 + (verifying_share.to_element() * challenge.0 * lambda_i)) + != (group_commitment_share.to_element() + + (verifying_share.to_element() * challenge.0 * lambda_i)) { return Err(Error::InvalidSignatureShare { culprit: identifier, @@ -96,7 +97,7 @@ where /// Compute the signature share for a signing operation. #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -fn compute_signature_share( +pub(super) fn compute_signature_share( signer_nonces: &round1::SigningNonces, binding_factor: BindingFactor, lambda_i: <<::Group as Group>::Field as Field>::Scalar, @@ -142,34 +143,38 @@ pub fn sign( return Err(Error::IncorrectCommitment); } + let (signing_package, signer_nonces, key_package) = + ::pre_sign(signing_package, signer_nonces, key_package)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &key_package.verifying_key, &[])?; + compute_binding_factor_list(&signing_package, &key_package.verifying_key, &[])?; let binding_factor: frost::BindingFactor = binding_factor_list .get(&key_package.identifier) .ok_or(Error::UnknownIdentifier)? .clone(); // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // Compute Lagrange coefficient. - let lambda_i = frost::derive_interpolating_value(key_package.identifier(), signing_package)?; + let lambda_i = frost::derive_interpolating_value(key_package.identifier(), &signing_package)?; // Compute the per-message challenge. - let challenge = challenge::( + let challenge = ::challenge( &group_commitment.0, &key_package.verifying_key, - signing_package.message.as_slice(), + signing_package.message(), )?; // Compute the Schnorr signature share. - let signature_share = compute_signature_share( - signer_nonces, + let signature_share = ::compute_signature_share( + &group_commitment, + &signer_nonces, binding_factor, lambda_i, - key_package, + &key_package, challenge, ); diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs index 7150b08..91d3ade 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -1,11 +1,12 @@ //! Schnorr signatures over prime order groups (or subgroups) use alloc::{string::ToString, vec::Vec}; +use derive_getters::Getters; use crate::{Ciphersuite, Element, Error, Field, Group, Scalar}; /// A Schnorr signature over some prime order group (or subgroup). -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Getters)] pub struct Signature { /// The commitment `R` to the signature nonce. pub(crate) R: Element, @@ -29,8 +30,10 @@ where Self { R, z } } - /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. - pub fn deserialize(bytes: &[u8]) -> Result> { + /// Converts default-encoded bytes as + /// [`Ciphersuite::SignatureSerialization`] into a `Signature`. + #[cfg(feature = "internals")] + pub fn default_deserialize(bytes: &[u8]) -> Result> { // To compute the expected length of the encoded point, encode the generator // and get its length. Note that we can't use the identity because it can be encoded // shorter in some cases (e.g. P-256, which uses SEC1 encoding). @@ -66,8 +69,14 @@ where }) } - /// Converts this signature to its byte serialization. - pub fn serialize(&self) -> Result, Error> { + /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. + pub fn deserialize(bytes: &[u8]) -> Result> { + C::deserialize_signature(bytes) + } + + /// Converts this signature to its default byte serialization. + #[cfg(feature = "internals")] + pub fn default_serialize(&self) -> Result, Error> { let mut bytes = Vec::::new(); bytes.extend(::serialize(&self.R)?.as_ref()); @@ -75,6 +84,11 @@ where Ok(bytes) } + + /// Converts this signature to its byte serialization. + pub fn serialize(&self) -> Result, Error> { + ::serialize_signature(self) + } } #[cfg(feature = "serde")] diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs index 4ba306e..93a096c 100644 --- a/frost-core/src/signing_key.rs +++ b/frost-core/src/signing_key.rs @@ -5,8 +5,8 @@ use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; use crate::{ - random_nonzero, serialization::SerializableScalar, Ciphersuite, Error, Field, Group, Scalar, - Signature, VerifyingKey, + random_nonzero, serialization::SerializableScalar, Challenge, Ciphersuite, Error, Field, Group, + Scalar, Signature, VerifyingKey, }; /// A signing key for a Schnorr signature on a FROST [`Ciphersuite::Group`]. @@ -40,13 +40,20 @@ where } /// Create a signature `msg` using this `SigningKey`. - pub fn sign(&self, mut rng: R, msg: &[u8]) -> Signature { - let k = random_nonzero::(&mut rng); + pub fn sign(&self, rng: R, message: &[u8]) -> Signature { + ::single_sign(self, rng, message) + } + + /// Create a signature `msg` using this `SigningKey` using the default + /// signing. + #[cfg(feature = "internals")] + pub fn default_sign(&self, mut rng: R, message: &[u8]) -> Signature { + let public = VerifyingKey::::from(*self); - let R = ::generator() * k; + let (k, R) = ::generate_nonce(&mut rng); // Generate Schnorr challenge - let c = crate::challenge::(&R, &VerifyingKey::::from(*self), msg).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); + let c: Challenge = ::challenge(&R, &public, message).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); let z = k + (c.0 * self.scalar); diff --git a/frost-core/src/tests/ciphersuite_generic.rs b/frost-core/src/tests/ciphersuite_generic.rs index 4528e9b..3271f26 100644 --- a/frost-core/src/tests/ciphersuite_generic.rs +++ b/frost-core/src/tests/ciphersuite_generic.rs @@ -4,12 +4,12 @@ use alloc::collections::BTreeMap; use crate as frost; +use crate::keys::SigningShare; use crate::round2::SignatureShare; use crate::{ keys::PublicKeyPackage, Error, Field, Group, Identifier, Signature, SigningKey, SigningPackage, VerifyingKey, }; -use alloc::borrow::ToOwned; use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; @@ -263,6 +263,8 @@ pub fn check_sign( signature_shares.clone(), ); + check_verify_signature_share(&pubkey_package, &signing_package, &signature_shares); + // Aggregate (also verifies the signature shares) let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?; @@ -497,7 +499,7 @@ where // for each signature before being aggregated. let mut pubkey_packages_by_participant = BTreeMap::new(); - check_part3_different_participants( + check_part3_errors( max_signers, round2_secret_packages.clone(), received_round1_packages.clone(), @@ -536,6 +538,33 @@ where check_sign(min_signers, key_packages, rng, pubkeys).unwrap() } +/// Check for error cases related to DKG part3. +fn check_part3_errors( + max_signers: u16, + round2_secret_packages: BTreeMap, frost::keys::dkg::round2::SecretPackage>, + received_round1_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + >, + received_round2_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round2::Package>, + >, +) { + check_part3_different_participants( + max_signers, + round2_secret_packages.clone(), + received_round1_packages.clone(), + received_round2_packages.clone(), + ); + check_part3_corrupted_share( + max_signers, + round2_secret_packages, + received_round1_packages, + received_round2_packages, + ); +} + /// Check that calling dkg::part3() with distinct sets of participants fail. fn check_part3_different_participants( max_signers: u16, @@ -573,6 +602,49 @@ fn check_part3_different_participants( } } +/// Check that calling dkg::part3() with a corrupted share fail, and the +/// culprit is correctly identified. +fn check_part3_corrupted_share( + max_signers: u16, + round2_secret_packages: BTreeMap, frost::keys::dkg::round2::SecretPackage>, + received_round1_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round1::Package>, + >, + received_round2_packages: BTreeMap< + Identifier, + BTreeMap, frost::keys::dkg::round2::Package>, + >, +) { + // For each participant, perform the third part of the DKG protocol. + // In practice, each participant will perform this on their own environments. + for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + + // Remove the first package from the map, and reinsert it with an unrelated + // Do the same for Round 2 packages + let mut received_round2_packages = + received_round2_packages[&participant_identifier].clone(); + let culprit = *received_round2_packages.keys().next().unwrap(); + let package = received_round2_packages.get_mut(&culprit).unwrap(); + let one = <::Group as Group>::Field::one(); + package.signing_share = SigningShare::new(package.signing_share().to_scalar() + one); + + let r = frost::keys::dkg::part3( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages, + ) + .expect_err("Should have failed due to corrupted share"); + assert_eq!( + r, + Error::InvalidSecretShare { + culprit: Some(culprit) + } + ) + } +} + /// Test FROST signing with trusted dealer with a Ciphersuite, using specified /// Identifiers. pub fn check_sign_with_dealer_and_identifiers( @@ -646,10 +718,12 @@ pub fn check_sign_with_dealer_and_identifiers( round1_secret_package: frost::keys::dkg::round1::SecretPackage, mut round1_packages: BTreeMap, frost::keys::dkg::round1::Package>, ) { + // Check if a corrupted proof of knowledge results in failure. let one = <::Group as Group>::Field::one(); // Corrupt a PoK let id = *round1_packages.keys().next().unwrap(); @@ -860,3 +934,36 @@ fn check_verifying_shares( assert_eq!(e.culprit(), Some(id)); assert_eq!(e, Error::InvalidSignatureShare { culprit: id }); } + +// Checks if `verify_signature_share()` works correctly. +fn check_verify_signature_share( + pubkeys: &PublicKeyPackage, + signing_package: &SigningPackage, + signature_shares: &BTreeMap, SignatureShare>, +) { + for (identifier, signature_share) in signature_shares { + frost::verify_signature_share( + *identifier, + pubkeys.verifying_shares().get(identifier).unwrap(), + signature_share, + signing_package, + pubkeys.verifying_key(), + ) + .expect("should pass"); + } + + for (identifier, signature_share) in signature_shares { + let one = <::Group as Group>::Field::one(); + // Corrupt share + let signature_share = SignatureShare::new(signature_share.to_scalar() + one); + + frost::verify_signature_share( + *identifier, + pubkeys.verifying_shares().get(identifier).unwrap(), + &signature_share, + signing_package, + pubkeys.verifying_key(), + ) + .expect_err("should have failed"); + } +} diff --git a/frost-core/src/traits.rs b/frost-core/src/traits.rs index 3e0d936..4e3f959 100644 --- a/frost-core/src/traits.rs +++ b/frost-core/src/traits.rs @@ -5,10 +5,18 @@ use core::{ ops::{Add, Mul, Sub}, }; -use alloc::vec::Vec; +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; use rand_core::{CryptoRng, RngCore}; -use crate::{Error, FieldError, GroupError, Signature, VerifyingKey}; +use crate::{ + challenge, + keys::{KeyPackage, PublicKeyPackage, VerifyingShare}, + random_nonzero, + round1::{self}, + round2::{self, SignatureShare}, + BindingFactor, Challenge, Error, FieldError, GroupCommitment, GroupError, Identifier, + Signature, SigningKey, SigningPackage, VerifyingKey, +}; /// A prime order finite field GF(q) over which all scalar values for our prime order group can be /// multiplied are defined. @@ -213,22 +221,205 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { None } - /// Verify a signature for this ciphersuite. The default implementation uses the "cofactored" - /// equation (it multiplies by the cofactor returned by [`Group::cofactor()`]). + // The following are optional methods that allow customizing steps of the + // protocol if required. + + /// Optional. Do regular (non-FROST) signing with a [`SigningKey`]. Called + /// by [`SigningKey::sign()`]. This is not used by FROST. Can be overriden + /// if required which is useful if FROST signing has been changed by the + /// other Ciphersuite trait methods and regular signing should be changed + /// accordingly to match. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + signing_key.default_sign(rng, message) + } + + /// Optional. Verify a signature for this ciphersuite. Called by + /// [`VerifyingKey::verify()`]. The default implementation uses the + /// "cofactored" equation (it multiplies by the cofactor returned by + /// [`Group::cofactor()`]). /// /// # Cryptographic Safety /// - /// You may override this to provide a tailored implementation, but if the ciphersuite defines it, - /// it must also multiply by the cofactor to comply with the RFC. Note that batch verification - /// (see [`crate::batch::Verifier`]) also uses the default implementation regardless whether a - /// tailored implementation was provided. + /// You may override this to provide a tailored implementation, but if the + /// ciphersuite defines it, it must also multiply by the cofactor to comply + /// with the RFC. Note that batch verification (see + /// [`crate::batch::Verifier`]) also uses the default implementation + /// regardless whether a tailored implementation was provided. fn verify_signature( - msg: &[u8], + message: &[u8], signature: &Signature, public_key: &VerifyingKey, ) -> Result<(), Error> { - let c = crate::challenge::(&signature.R, public_key, msg)?; + let (message, signature, public_key) = ::pre_verify(message, signature, public_key)?; + + let c = ::challenge(&signature.R, &public_key, &message)?; + + public_key.verify_prehashed(c, &signature) + } + + /// Optional. Pre-process [`round2::sign()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Borrowed(key_package), + )) + } + + /// Optional. Pre-process [`crate::aggregate()`] and + /// [`crate::verify_signature_share()`] inputs. In the latter case, "dummy" + /// container BTreeMap and PublicKeyPackage are passed with the relevant + /// values. The default implementation returns them as-is. [`Cow`] is used + /// so implementations can choose to return the same passed reference or a + /// modified clone. + #[allow(clippy::type_complexity)] + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, round2::SignatureShare>, + public_key_package: &'a PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap, round2::SignatureShare>>, + Cow<'a, PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Borrowed(public_key_package), + )) + } + + /// Optional. Pre-process [`VerifyingKey::verify()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_verify<'a>( + msg: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result< + ( + Cow<'a, [u8]>, + Cow<'a, Signature>, + Cow<'a, VerifyingKey>, + ), + Error, + > { + Ok(( + Cow::Borrowed(msg), + Cow::Borrowed(signature), + Cow::Borrowed(public_key), + )) + } + + /// Optional. Generate a nonce and a commitment to it. Used by + /// [`SigningKey`] for regular (non-FROST) signing and internally by the DKG + /// to generate proof-of-knowledge signatures. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + (k, R) + } + + /// Optional. Generates the challenge as is required for Schnorr signatures. + /// Called by [`round2::sign()`] and [`crate::aggregate()`]. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + challenge(R, verifying_key, message) + } + + /// Optional. Compute the signature share for a particular signer on a given + /// challenge. Called by [`round2::sign()`]. + fn compute_signature_share( + _group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + round2::compute_signature_share( + signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Optional. Verify a signing share. Called by [`crate::aggregate()`] if + /// cheater detection is enabled. + fn verify_share( + _group_commitment: &GroupCommitment, + signature_share: &SignatureShare, + identifier: Identifier, + group_commitment_share: &round1::GroupCommitmentShare, + verifying_share: &VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + signature_share.verify( + identifier, + group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Optional. Converts a signature to its + /// [`Ciphersuite::SignatureSerialization`] in bytes. + /// + /// The default implementation serializes a signature by serializing its `R` + /// point and `z` component independently, and then concatenating them. + fn serialize_signature(signature: &Signature) -> Result, Error> { + signature.default_serialize() + } + + /// Optional. Converts bytes as [`Ciphersuite::SignatureSerialization`] into + /// a `Signature`. + /// + /// The default implementation assumes the serialization is a serialized `R` + /// point followed by a serialized `z` component with no padding or extra + /// fields. + fn deserialize_signature(bytes: &[u8]) -> Result, Error> { + Signature::::default_deserialize(bytes) + } - public_key.verify_prehashed(c, signature) + /// Post-process the output of the DKG for a given participant. + fn post_dkg( + key_package: KeyPackage, + public_key_package: PublicKeyPackage, + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + Ok((key_package, public_key_package)) } } diff --git a/frost-core/src/verifying_key.rs b/frost-core/src/verifying_key.rs index c3a1cfc..24f0540 100644 --- a/frost-core/src/verifying_key.rs +++ b/frost-core/src/verifying_key.rs @@ -51,6 +51,8 @@ where /// Verify a purported `signature` with a pre-hashed [`Challenge`] made by this verification /// key. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn verify_prehashed( &self, challenge: Challenge, diff --git a/frost-ed25519/Cargo.toml b/frost-ed25519/Cargo.toml index 5c1833b..add2afc 100644 --- a/frost-ed25519/Cargo.toml +++ b/frost-ed25519/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -25,15 +25,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } rand_core = "0.6" sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } ed25519-dalek = "2.0.0" insta = { version = "1.31.0", features = ["yaml"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/frost-ed25519/dkg.md b/frost-ed25519/dkg.md index 797422c..19e2433 100644 --- a/frost-ed25519/dkg.md +++ b/frost-ed25519/dkg.md @@ -3,7 +3,7 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from +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 diff --git a/frost-ed25519/tests/helpers/samples.json b/frost-ed25519/tests/helpers/samples.json index 3402fbe..a61e0c4 100644 --- a/frost-ed25519/tests/helpers/samples.json +++ b/frost-ed25519/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "5866666666666666666666666666666666666666666666666666666666666666498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "5866666666666666666666666666666666666666666666666666666666666666", "element2": "c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ed25519/tests/helpers/samples.rs b/frost-ed25519/tests/helpers/samples.rs index 6f22aed..4d785e5 100644 --- a/frost-ed25519/tests/helpers/samples.rs +++ b/frost-ed25519/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-ed448/Cargo.toml b/frost-ed448/Cargo.toml index ead6ef3..17ba4b5 100644 --- a/frost-ed448/Cargo.toml +++ b/frost-ed448/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -24,15 +24,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" ed448-goldilocks = { version = "0.9.0" } -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } rand_core = "0.6" sha3 = { version = "0.10.6", default-features = false } [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } lazy_static = "1.4" insta = { version = "1.31.0", features = ["yaml"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/frost-ed448/dkg.md b/frost-ed448/dkg.md index 2d60b14..6471d76 100644 --- a/frost-ed448/dkg.md +++ b/frost-ed448/dkg.md @@ -3,7 +3,7 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from +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 diff --git a/frost-ed448/tests/helpers/samples.json b/frost-ed448/tests/helpers/samples.json index 36e8628..f93c3e9 100644 --- a/frost-ed448/tests/helpers/samples.json +++ b/frost-ed448/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69004d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00", "element1": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900", "element2": "ed8693eacdfbeada6ba0cdd1beb2bcbb98302a3a8365650db8c4d88a726de3b7d74d8835a0d76e03b0c2865020d659b38d04d74a63e905ae80", "scalar1": "4d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00" -} \ No newline at end of file +} diff --git a/frost-ed448/tests/helpers/samples.rs b/frost-ed448/tests/helpers/samples.rs index ec55673..afb26d5 100644 --- a/frost-ed448/tests/helpers/samples.rs +++ b/frost-ed448/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-p256/Cargo.toml b/frost-p256/Cargo.toml index b55969e..5b94d48 100644 --- a/frost-p256/Cargo.toml +++ b/frost-p256/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -25,15 +25,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" p256 = { version = "0.13.0", features = ["hash2curve"], default-features = false } -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } rand_core = "0.6" sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } lazy_static = "1.4" diff --git a/frost-p256/dkg.md b/frost-p256/dkg.md index 9a3c432..f3dbb23 100644 --- a/frost-p256/dkg.md +++ b/frost-p256/dkg.md @@ -3,7 +3,7 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from +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 diff --git a/frost-p256/src/lib.rs b/frost-p256/src/lib.rs index 3e798f4..35c2620 100644 --- a/frost-p256/src/lib.rs +++ b/frost-p256/src/lib.rs @@ -8,7 +8,6 @@ extern crate alloc; -use alloc::borrow::ToOwned; use alloc::collections::BTreeMap; use frost_rerandomized::RandomizedCiphersuite; @@ -159,9 +158,9 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { output } -fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { let mut u = [P256ScalarField::zero()]; - hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + hash_to_field::, Scalar>(&[msg], domain, &mut u) .expect("should never return error according to error cases described in ExpandMsgXmd"); u[0] } @@ -188,21 +187,21 @@ impl Ciphersuite for P256Sha256 { /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) } /// H2 for FROST(P-256, SHA-256) /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "chal").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"chal"], m) } /// H3 for FROST(P-256, SHA-256) /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.4-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) } /// H4 for FROST(P-256, SHA-256) @@ -221,25 +220,19 @@ impl Ciphersuite for P256Sha256 { /// HDKG for FROST(P-256, SHA-256) fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "dkg").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) } /// HID for FROST(P-256, SHA-256) fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "id").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) } } impl RandomizedCiphersuite for P256Sha256 { fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "randomizer").as_bytes(), + &[CONTEXT_STRING.as_bytes(), b"randomizer"], m, )) } diff --git a/frost-p256/tests/helpers/samples.json b/frost-p256/tests/helpers/samples.json index 928e355..3fe4c69 100644 --- a/frost-p256/tests/helpers/samples.json +++ b/frost-p256/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1", "element1": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", "element2": "037cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc47669978", "scalar1": "aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1" -} \ No newline at end of file +} diff --git a/frost-p256/tests/helpers/samples.rs b/frost-p256/tests/helpers/samples.rs index 340bc76..432af47 100644 --- a/frost-p256/tests/helpers/samples.rs +++ b/frost-p256/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-rerandomized/Cargo.toml b/frost-rerandomized/Cargo.toml index 1b011eb..da4d244 100644 --- a/frost-rerandomized/Cargo.toml +++ b/frost-rerandomized/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -25,7 +25,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] derive-getters = "0.5.0" document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = [ +frost-core = { path = "../frost-core", version = "2.0.0", features = [ "internals" ], default-features = false } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/frost-ristretto255/Cargo.toml b/frost-ristretto255/Cargo.toml index 677ecc9..e666dc8 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = ["Deirdre Connolly ", "Chelsea Komlo ", "Conrado Gouvea "] readme = "README.md" license = "MIT OR Apache-2.0" @@ -21,15 +21,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] curve25519-dalek = { version = "=4.1.3", features = ["rand_core"] } document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } rand_core = "0.6" sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } lazy_static = "1.4" diff --git a/frost-ristretto255/dkg.md b/frost-ristretto255/dkg.md index 481f24a..18954d4 100644 --- a/frost-ristretto255/dkg.md +++ b/frost-ristretto255/dkg.md @@ -3,7 +3,7 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from +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 diff --git a/frost-ristretto255/tests/helpers/samples.json b/frost-ristretto255/tests/helpers/samples.json index bb80d1b..1fff133 100644 --- a/frost-ristretto255/tests/helpers/samples.json +++ b/frost-ristretto255/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", "element2": "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ristretto255/tests/helpers/samples.rs b/frost-ristretto255/tests/helpers/samples.rs index f598c53..da9e991 100644 --- a/frost-ristretto255/tests/helpers/samples.rs +++ b/frost-ristretto255/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-secp256k1-tr/Cargo.toml b/frost-secp256k1-tr/Cargo.toml new file mode 100644 index 0000000..d769c49 --- /dev/null +++ b/frost-secp256k1-tr/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "frost-secp256k1-tr" +edition = "2021" +# When releasing to crates.io: +# - Update CHANGELOG.md +# - Create git tag. +version = "2.0.0" +authors = [ + "Deirdre Connolly ", + "Chelsea Komlo ", + "Conrado Gouvea " +] +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/ZcashFoundation/frost" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "threshold", "signature"] +description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST and Taproot." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +document-features = "0.2.7" +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } +k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"], default-features = false } +rand_core = "0.6" +sha2 = { version = "0.10.2", default-features = false } + +[dev-dependencies] +criterion = "0.5" +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } +insta = { version = "1.31.0", features = ["yaml"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +lazy_static = "1.4" +proptest = "1.0" +rand = "0.8" +rand_chacha = "0.3" +secp256k1 = "0.30.0" +serde_json = "1.0" + +[features] +nightly = [] +default = ["serialization", "cheater-detection", "std"] +#! ## 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 +# when using criterion-specific flags +bench = false + +[[bench]] +name = "bench" +harness = false diff --git a/frost-secp256k1-tr/README.md b/frost-secp256k1-tr/README.md new file mode 100644 index 0000000..f4d2205 --- /dev/null +++ b/frost-secp256k1-tr/README.md @@ -0,0 +1,121 @@ +An implementation of Schnorr signatures on the secp256k1 curve (Taproot) for both single and threshold numbers +of signers (FROST). + +## Example: key generation with trusted dealer and FROST signing + +Creating a key with a trusted dealer and splitting into shares; then signing a message +and aggregating the signature. Note that the example just simulates a distributed +scenario in a single thread and it abstracts away any communication between peers. + + +```rust +# // ANCHOR: tkg_gen +use frost_secp256k1_tr as frost; +use rand::thread_rng; +use std::collections::BTreeMap; + +let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, +)?; +# // ANCHOR_END: tkg_gen + +// Verifies the secret shares from the dealer and store them in a BTreeMap. +// In practice, the KeyPackages must be sent to its respective participants +// through a confidential and authenticated channel. +let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + +for (identifier, secret_share) in shares { + # // ANCHOR: tkg_verify + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + # // ANCHOR_END: tkg_verify + key_packages.insert(identifier, key_package); +} + +let mut nonces_map = BTreeMap::new(); +let mut commitments_map = BTreeMap::new(); + +//////////////////////////////////////////////////////////////////////////// +// Round 1: generating nonces and signing commitments for each participant +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( + key_package.signing_share(), + &mut rng, + ); + # // ANCHOR_END: round1_commit + // In practice, the nonces must be kept by the participant to use in the + // next round, while the commitment must be sent to the coordinator + // (or to every other participant if there is no coordinator) using + // an authenticated channel. + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); +} + +// This is what the signature aggregator / coordinator needs to do: +// - decide what message to sign +// - take one (unused) commitment per signing participant +let mut signature_shares = BTreeMap::new(); +# // ANCHOR: round2_package +let message = "message to sign".as_bytes(); +# // In practice, the SigningPackage must be sent to all participants +# // involved in the current signing (at least min_signers participants), +# // using an authenticate channel (and confidential if the message is secret). +let signing_package = frost::SigningPackage::new(commitments_map, message); +# // ANCHOR_END: round2_package + +//////////////////////////////////////////////////////////////////////////// +// Round 2: each participant generates their signature share +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + + let nonces = &nonces_map[participant_identifier]; + + // Each participant generates their signature share. + # // ANCHOR: round2_sign + let signature_share = frost::round2::sign(&signing_package, nonces, key_package)?; + # // ANCHOR_END: round2_sign + + // In practice, the signature share must be sent to the Coordinator + // using an authenticated channel. + signature_shares.insert(*participant_identifier, signature_share); +} + +//////////////////////////////////////////////////////////////////////////// +// Aggregation: collects the signing shares from all participants, +// generates the final signature. +//////////////////////////////////////////////////////////////////////////// + +// Aggregate (also verifies the signature shares) +# // ANCHOR: aggregate +let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?; +# // ANCHOR_END: aggregate + + +// Check that the threshold signature can be verified by the group public +// key (the verification key). +# // ANCHOR: verify +let is_signature_valid = pubkey_package + .verifying_key() + .verify(message, &group_signature) + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); + +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/benches/bench.rs b/frost-secp256k1-tr/benches/bench.rs new file mode 100644 index 0000000..e9097bd --- /dev/null +++ b/frost-secp256k1-tr/benches/bench.rs @@ -0,0 +1,19 @@ +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(); + + frost_core::benches::bench_batch_verify::(c, "secp256k1", &mut rng); +} + +fn bench_secp256k1_sign(c: &mut Criterion) { + let mut rng = thread_rng(); + + frost_core::benches::bench_sign::(c, "secp256k1", &mut rng); +} + +criterion_group!(benches, bench_secp256k1_batch_verify, bench_secp256k1_sign); +criterion_main!(benches); diff --git a/frost-secp256k1-tr/dkg.md b/frost-secp256k1-tr/dkg.md new file mode 100644 index 0000000..9643d37 --- /dev/null +++ b/frost-secp256k1-tr/dkg.md @@ -0,0 +1,168 @@ +# Distributed Key Generation (DKG) + +The DKG module supports generating FROST key shares in a distributed manner, +without a trusted dealer. + +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. + + +## Example + +```rust +# // ANCHOR: dkg_import +use rand::thread_rng; +use std::collections::BTreeMap; + +use frost_secp256k1_tr as frost; + +let mut rng = thread_rng(); + +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 1 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 1 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round1_secret_packages = BTreeMap::new(); + +// Keep track of all round 1 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through some communication channel. +let mut received_round1_packages = BTreeMap::new(); + +// For each participant, perform the first part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + # // ANCHOR: dkg_part1 + let (round1_secret_package, round1_package) = frost::keys::dkg::part1( + participant_identifier, + max_signers, + min_signers, + &mut rng, + )?; + # // ANCHOR_END: dkg_part1 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round1_secret_packages.insert(participant_identifier, round1_secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + for receiver_participant_index in 1..=max_signers { + if receiver_participant_index == participant_index { + continue; + } + let receiver_participant_identifier: frost::Identifier = receiver_participant_index + .try_into() + .expect("should be nonzero"); + received_round1_packages + .entry(receiver_participant_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round1_package.clone()); + } +} + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 2 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 2 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round2_secret_packages = BTreeMap::new(); + +// Keep track of all round 2 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through 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_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + # // ANCHOR: dkg_part2 + let (round2_secret_package, round2_packages) = + frost::keys::dkg::part2(round1_secret_package, round1_packages)?; + # // ANCHOR_END: dkg_part2 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round2_secret_packages.insert(participant_identifier, round2_secret_package); + + // "Send" the round 2 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through 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 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's long-lived key package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut key_packages = BTreeMap::new(); + +// Keep track of each participant's public key package. +// In practice, if there is a Coordinator, only they need to store the set. +// If there is not, then all candidates must store their own sets. +// All participants will have the same exact public key package. +let mut pubkey_packages = BTreeMap::new(); + +// For each participant, perform the third part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round2_secret_package = &round2_secret_packages[&participant_identifier]; + let round1_packages = &received_round1_packages[&participant_identifier]; + let round2_packages = &received_round2_packages[&participant_identifier]; + # // ANCHOR: dkg_part3 + let (key_package, pubkey_package) = frost::keys::dkg::part3( + round2_secret_package, + round1_packages, + round2_packages, + )?; + # // ANCHOR_END: dkg_part3 + key_packages.insert(participant_identifier, key_package); + pubkey_packages.insert(participant_identifier, pubkey_package); +} + +// With its own key package and the pubkey package, each participant can now proceed +// to sign with FROST. +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/src/keys/dkg.rs b/frost-secp256k1-tr/src/keys/dkg.rs new file mode 100644 index 0000000..9ea40b2 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/dkg.rs @@ -0,0 +1,103 @@ +#![doc = include_str!("../../dkg.md")] +use super::*; + +/// DKG Round 1 structures. +pub mod round1 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the first and second parts of the DKG protocol (round 1). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round1::SecretPackage; + + /// The package that must be broadcast by each participant to all other participants + /// between the first and second parts of the DKG protocol (round 1). + pub type Package = frost::keys::dkg::round1::Package; +} + +/// DKG Round 2 structures. +pub mod round2 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the second and third parts of the DKG protocol (round 2). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round2::SecretPackage; + + /// A package that must be sent by each participant to some other participants + /// in Round 2 of the DKG protocol. Note that there is one specific package + /// for each specific recipient, in contrast to Round 1. + /// + /// # Security + /// + /// The package must be sent on an *confidential* and *authenticated* channel. + pub type Package = frost::keys::dkg::round2::Package; +} + +/// Performs the first part of the distributed key generation protocol +/// for the given participant. +/// +/// It returns the [`round1::SecretPackage`] that must be kept in memory +/// by the participant for the other steps, and the [`round1::Package`] that +/// must be sent to each other participant in the DKG run. +pub fn part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. +/// +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. +pub fn part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::dkg::part2(secret_package, round1_packages) +} + +/// Performs the third and final part of the distributed key generation protocol +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`KeyPackage`] that has the long-lived key share for the +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. +pub fn part3( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages) +} diff --git a/frost-secp256k1-tr/src/keys/refresh.rs b/frost-secp256k1-tr/src/keys/refresh.rs new file mode 100644 index 0000000..c270fc2 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/refresh.rs @@ -0,0 +1,35 @@ +//! 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) + +use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; +use alloc::vec::Vec; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refreshes shares using a trusted dealer +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, + ) +} + +/// Each participant refreshed their shares +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} diff --git a/frost-secp256k1-tr/src/keys/repairable.rs b/frost-secp256k1-tr/src/keys/repairable.rs new file mode 100644 index 0000000..9b53803 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/repairable.rs @@ -0,0 +1,103 @@ +//! Repairable Threshold Scheme +//! +//! Implements the Repairable Threshold Scheme (RTS) from . +//! The RTS is used to help a signer (participant) repair their lost share. This is achieved +//! using a subset of the other signers know here as `helpers`. + +use alloc::collections::BTreeMap; + +// 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::{Error, Secp256K1Sha256TR}; + +use super::{SecretShare, VerifiableSecretSharingCommitment}; + +/// Step 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`. +/// +/// Returns a BTreeMap mapping which value should be sent to which participant. +pub fn repair_share_step_1( + helpers: &[Identifier], + share_i: &SecretShare, + rng: &mut R, + participant: Identifier, +) -> Result, Error> { + frost::keys::repairable::repair_share_step_1(helpers, share_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`. +/// +/// Returns a scalar +pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { + frost::keys::repairable::repair_share_step_2::(deltas_j) +} + +/// Step 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], + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, +) -> SecretShare { + frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) +} + +#[cfg(test)] +mod tests { + + use lazy_static::lazy_static; + use rand::thread_rng; + use serde_json::Value; + + use crate::Secp256K1Sha256TR; + + lazy_static! { + pub static ref REPAIR_SHARE: Value = + serde_json::from_str(include_str!("../../tests/helpers/repair-share.json").trim()) + .unwrap(); + } + + #[test] + fn check_repair_share_step_1() { + let rng = thread_rng(); + + frost_core::tests::repairable::check_repair_share_step_1::(rng); + } + + #[test] + fn check_repair_share_step_2() { + frost_core::tests::repairable::check_repair_share_step_2::( + &REPAIR_SHARE, + ); + } + + #[test] + fn check_repair_share_step_3() { + let rng = thread_rng(); + frost_core::tests::repairable::check_repair_share_step_3::( + 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::< + Secp256K1Sha256TR, + _, + >(rng); + } +} diff --git a/frost-secp256k1-tr/src/lib.rs b/frost-secp256k1-tr/src/lib.rs new file mode 100644 index 0000000..5310ced --- /dev/null +++ b/frost-secp256k1-tr/src/lib.rs @@ -0,0 +1,918 @@ +#![cfg_attr(not(feature = "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!()] + +extern crate alloc; + +use alloc::vec; +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; + +use frost_rerandomized::RandomizedCiphersuite; +use k256::elliptic_curve::ops::Reduce; +use k256::{ + elliptic_curve::{ + bigint::U256, + group::prime::PrimeCurveAffine, + hash2curve::{hash_to_field, ExpandMsgXmd}, + point::AffineCoordinates, + sec1::{FromEncodedPoint, ToEncodedPoint}, + Field as FFField, PrimeField, + }, + AffinePoint, ProjectivePoint, Scalar, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use frost_core::{self as frost, random_nonzero}; + +use keys::EvenY; +use keys::Tweak; + +#[cfg(test)] +mod tests; + +// Re-exports in our public API +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{ + Challenge, Ciphersuite, Element, Field, FieldError, Group, GroupCommitment, GroupError, +}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct Secp256K1ScalarField; + +impl Field for Secp256K1ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`Scalar`]'s Eq/PartialEq does a constant-time comparison + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert().unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let field_bytes: &k256::FieldBytes = buf.into(); + match Scalar::from_repr(*field_bytes).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + let mut array = Self::serialize(scalar); + array.reverse(); + array + } +} + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Secp256K1Group; + +impl Group for Secp256K1Group { + type Field = Secp256K1ScalarField; + + type Element = ProjectivePoint; + + /// [SEC 1][1] serialization of a compressed point in secp256k1 takes 33 bytes + /// (1-byte prefix and 32 bytes for the coordinate). + /// + /// Note that, in the SEC 1 spec, the identity is encoded as a single null byte; + /// but here we pad with zeroes. This is acceptable as the identity _should_ never + /// be serialized in FROST, else we error. + /// + /// [1]: https://secg.org/sec1-v2.pdf + type Serialization = [u8; 33]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + ProjectivePoint::IDENTITY + } + + fn generator() -> Self::Element { + ProjectivePoint::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + let mut fixed_serialized = [0; 33]; + let serialized_point = element.to_affine().to_encoded_point(true); + let serialized = serialized_point.as_bytes(); + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let encoded_point = + k256::EncodedPoint::from_bytes(buf).map_err(|_| GroupError::MalformedElement)?; + + match Option::::from(AffinePoint::from_encoded_point(&encoded_point)) { + Some(point) => { + if point.is_identity().into() { + // This is actually impossible since the identity is encoded a a single byte + // which will never happen since we receive a 33-byte buffer. + // We leave the check for consistency. + Err(GroupError::InvalidIdentityElement) + } else { + Ok(ProjectivePoint::from(point)) + } + } + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for i in inputs { + h.update(i); + } + let mut output = [0u8; 32]; + output.copy_from_slice(h.finalize().as_slice()); + output +} + +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { + let mut u = [Secp256K1ScalarField::zero()]; + hash_to_field::, Scalar>(&[msg], domain, &mut u) + .expect("should never return error according to error cases described in ExpandMsgXmd"); + u[0] +} + +/// Context string from the ciphersuite in the [spec]. +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-1 +const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-TR-v1"; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Secp256K1Sha256TR; + +/// Digest the hasher to a Scalar +fn hasher_to_scalar(hasher: Sha256) -> Scalar { + // This is acceptable because secp256k1 curve order is close to 2^256, + // and the input is uniformly random since it is a hash output, therefore + // the bias is negligibly small. + Scalar::reduce(U256::from_be_slice(&hasher.finalize())) +} + +/// Create a BIP340 compliant tagged hash +fn tagged_hash(tag: &str) -> Sha256 { + let mut hasher = Sha256::new(); + let mut tag_hasher = Sha256::new(); + tag_hasher.update(tag.as_bytes()); + let tag_hash = tag_hasher.finalize(); + hasher.update(tag_hash); + hasher.update(tag_hash); + hasher +} + +/// Create a BIP341 compliant taproot tweak +fn tweak>( + public_key: &<::Group as Group>::Element, + merkle_root: Option, +) -> Scalar { + match merkle_root { + None => Secp256K1ScalarField::zero(), + Some(root) => { + let mut hasher = tagged_hash("TapTweak"); + hasher.update(public_key.to_affine().x()); + hasher.update(root.as_ref()); + hasher_to_scalar(hasher) + } + } +} + +// Negate a Nonce +fn negate_nonce(nonce: &frost_core::round1::Nonce) -> frost_core::round1::Nonce { + frost_core::round1::Nonce::::from_scalar(-nonce.to_scalar()) +} + +// Negate a SigningNonces +fn negate_nonces(signing_nonces: &round1::SigningNonces) -> round1::SigningNonces { + // TODO: this recomputes commitments which is expensive, and not needed. + // Create an `internals` SigningNonces::from_nonces_and_commitments or + // something similar. + round1::SigningNonces::from_nonces( + negate_nonce(signing_nonces.hiding()), + negate_nonce(signing_nonces.binding()), + ) +} + +impl Ciphersuite for Secp256K1Sha256TR { + const ID: &'static str = CONTEXT_STRING; + + type Group = Secp256K1Group; + + type HashOutput = [u8; 32]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) + } + + /// H2 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + let mut hasher = tagged_hash("BIP0340/challenge"); + hasher.update(m); + hasher_to_scalar(hasher) + } + + /// H3 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) + } + + /// H4 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(secp256k1, SHA-256) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) + } + + /// HID for FROST(secp256k1, SHA-256) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) + } + + // Sign, negating the key if required by BIP-340. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + let signing_key = signing_key.into_even_y(None); + signing_key.default_sign(rng, message) + } + + // Preprocess sign inputs, negating the keys in the KeyPackage if required + // by BIP-340. + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a keys::KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, keys::KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Owned(key_package.clone().into_even_y(None)), + )) + } + + // Preprocess sign inputs, negating the keys in the PublicKeyPackage if + // required by BIP-340. + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, + public_key_package: &'a keys::PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap>, + Cow<'a, keys::PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Owned(public_key_package.clone().into_even_y(None)), + )) + } + + // Preprocess verify inputs, negating the VerifyingKey and `signature.R` if required by + // BIP-340. + fn pre_verify<'a>( + message: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result<(Cow<'a, [u8]>, Cow<'a, Signature>, Cow<'a, VerifyingKey>), Error> { + let public_key = public_key.into_even_y(None); + let signature = signature.into_even_y(None); + Ok(( + Cow::Borrowed(message), + Cow::Owned(signature), + Cow::Owned(public_key), + )) + } + + // Generate a nonce, negating it if required by BIP-340. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + if R.to_affine().y_is_odd().into() { + (-k, -R) + } else { + (k, R) + } + } + + // Compute the challenge. Per BIP-340, only the X coordinate of R and + // verifying_key are hashed, unlike vanilla FROST. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + let mut preimage = vec![]; + preimage.extend_from_slice(&R.to_affine().x()); + preimage.extend_from_slice(&verifying_key.to_element().to_affine().x()); + preimage.extend_from_slice(message); + Ok(Challenge::from_scalar(S::H2(&preimage[..]))) + } + + /// Compute a signature share, negating the nonces if required by BIP-340. + fn compute_signature_share( + group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: frost::BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &frost::keys::KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + let signer_nonces = if !group_commitment.has_even_y() { + negate_nonces(signer_nonces) + } else { + signer_nonces.clone() + }; + + frost::round2::compute_signature_share( + &signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Verify a signature share, negating the group commitment share if + /// required by BIP-340. + fn verify_share( + group_commitment: &GroupCommitment, + signature_share: &frost_core::round2::SignatureShare, + identifier: Identifier, + group_commitment_share: &frost_core::round1::GroupCommitmentShare, + verifying_share: &frost_core::keys::VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + let group_commitment_share = if !group_commitment.has_even_y() { + frost_core::round1::GroupCommitmentShare::from_element( + -group_commitment_share.to_element(), + ) + } else { + *group_commitment_share + }; + signature_share.verify( + identifier, + &group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Serialize a signature in compact BIP340 format, with an x-only R point. + fn serialize_signature(signature: &Signature) -> Result, Error> { + let R_bytes = Self::Group::serialize(signature.R())?; + let z_bytes = ::Field::serialize(signature.z()); + + let mut bytes = vec![0u8; 64]; + bytes[..32].copy_from_slice(&R_bytes[1..]); + bytes[32..].copy_from_slice(&z_bytes); + Ok(bytes) + } + + /// Deserialize a signature in compact BIP340 format, with an x-only R point. + fn deserialize_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(Error::MalformedSignature); + } + + let mut R_bytes = [0u8; 33]; + R_bytes[0] = 0x02; // taproot signatures always have an even R point + R_bytes[1..].copy_from_slice(&bytes[..32]); + + let mut z_bytes = [0u8; 32]; + z_bytes.copy_from_slice(&bytes[32..]); + + let R = Self::Group::deserialize(&R_bytes)?; + let z = ::Field::deserialize(&z_bytes)?; + + Ok(Signature::new(R, z)) + } + + /// Post-process the DKG output. We add an unusable taproot tweak to the + /// group key computed by a DKG run, to prevent peers from inserting rogue + /// tapscript tweaks into the group's joint public key. + fn post_dkg( + key_package: keys::KeyPackage, + public_key_package: keys::PublicKeyPackage, + ) -> Result<(keys::KeyPackage, keys::PublicKeyPackage), Error> { + // From BIP-341: + // > If the spending conditions do not require a script path, the output + // > key should commit to an unspendable script path instead of having + // > no script path. This can be achieved by computing the output key + // > point as Q = P + int(hashTapTweak(bytes(P)))G. + let merkle_root = [0u8; 0]; + Ok(( + key_package.tweak(Some(&merkle_root)), + public_key_package.tweak(Some(&merkle_root)), + )) + } +} + +impl RandomizedCiphersuite for Secp256K1Sha256TR { + fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar( + &[CONTEXT_STRING.as_bytes(), b"randomizer"], + m, + )) + } +} + +type S = Secp256K1Sha256TR; + +/// A FROST(secp256k1, SHA-256) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(secp256k1, SHA-256) keys, key generation, key shares. +pub mod keys { + use super::*; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, S>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(secp256k1, SHA-256) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare; + + /// A FROST(secp256k1, SHA-256) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; + + /// Trait for ensuring the group public key has an even Y coordinate. + /// + /// In BIP-320, public keys are encoded with only the X coordinate, which + /// means that two Y coordinates are possible. The specification says that + /// the coordinate which is even must be used. Alternatively, something + /// equivalent can be accomplished by simply converting any existing + /// (non-encoded) public key to have an even Y coordinate. + /// + /// This trait is used to enable this procedure, by changing the private and + /// public keys to ensure that the public key has a even Y coordinate. This + /// is done by simply negating both keys if Y is even (in a field, negating + /// is equivalent to computing p - x where p is the prime modulus. Since p + /// is odd, if x is odd then the result will be even). Fortunately this + /// works even after Shamir secret sharing, in the individual signing and + /// verifying shares, since it's linear. + pub trait EvenY { + /// Return if the given type has a group public key with an even Y + /// coordinate. + fn has_even_y(&self) -> bool; + + /// Convert the given type to make sure the group public key has an even + /// Y coordinate. `is_even` can be specified if evenness was already + /// determined beforehand. + fn into_even_y(self, is_even: Option) -> Self; + } + + impl EvenY for PublicKeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate verifying key + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = self + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(-vs.to_element()); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } else { + self + } + } + } + + impl EvenY for KeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate all components + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + let signing_share = SigningShare::new(-self.signing_share().to_scalar()); + let verifying_share = VerifyingShare::new(-self.verifying_share().to_element()); + KeyPackage::new( + *self.identifier(), + signing_share, + verifying_share, + verifying_key, + *self.min_signers(), + ) + } else { + self + } + } + } + + impl EvenY for VerifyingKey { + fn has_even_y(&self) -> bool { + (!self.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + VerifyingKey::new(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for GroupCommitment { + fn has_even_y(&self) -> bool { + (!self.clone().to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::from_element(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for Signature { + fn has_even_y(&self) -> bool { + (!self.R().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::new(-*self.R(), *self.z()) + } else { + self + } + } + } + + impl EvenY for SigningKey { + fn has_even_y(&self) -> bool { + (!Into::::into(self) + .to_element() + .to_affine() + .y_is_odd()) + .into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + SigningKey::from_scalar(-self.to_scalar()) + .expect("the original SigningKey must be nonzero") + } else { + self + } + } + } + + /// Trait for tweaking a key component following BIP-341 + pub trait Tweak: EvenY { + /// Convert the given type to add a tweak. + fn tweak>(self, merkle_root: Option) -> Self; + } + + impl Tweak for PublicKeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let public_key_package = self.into_even_y(None); + let verifying_key = + VerifyingKey::new(public_key_package.verifying_key().to_element() + tp); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = public_key_package + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(vs.to_element() + tp); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } + } + + impl Tweak for KeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let key_package = self.into_even_y(None); + let verifying_key = VerifyingKey::new(key_package.verifying_key().to_element() + tp); + let signing_share = SigningShare::new(key_package.signing_share().to_scalar() + t); + let verifying_share = + VerifyingShare::new(key_package.verifying_share().to_element() + tp); + KeyPackage::new( + *key_package.identifier(), + signing_share, + verifying_share, + verifying_key, + *key_package.min_signers(), + ) + } + } + + pub mod dkg; + pub mod repairable; +} + +/// FROST(secp256k1, SHA-256) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(secp256k1, SHA-256) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(secp256k1, SHA-256) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use keys::Tweak; + + use super::*; + + /// A FROST(secp256k1, SHA-256) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare; + + /// Performed once by each participant selected for the signing operation. + /// + /// Receives the message to be signed and a set of signing commitments and a set + /// of randomizing commitments to be used in that signing operation, including + /// that for this participant. + /// + /// Assumes the participant has already determined which nonce corresponds with + /// the commitment that was assigned by the coordinator in the SigningPackage. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + + /// Same as [`sign()`], but using a Taproot tweak as specified in BIP-341. + pub fn sign_with_tweak( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + merkle_root: Option<&[u8]>, + ) -> Result { + if merkle_root.is_some() { + let key_package = key_package.clone().tweak(merkle_root); + frost::round2::sign(signing_package, signer_nonces, &key_package) + } else { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + } +} + +/// A Schnorr signature on FROST(secp256k1, SHA-256). +pub type Signature = frost_core::Signature; + +/// Verifies each FROST(secp256k1, SHA-256) participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain Schnorr +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, public_key_package) +} + +/// Same as [`aggregate()`], but using a Taproot tweak as specified in BIP-341. +pub fn aggregate_with_tweak( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, + merkle_root: Option<&[u8]>, +) -> Result { + if merkle_root.is_some() { + let public_key_package = public_key_package.clone().tweak(merkle_root); + frost::aggregate(signing_package, signature_shares, &public_key_package) + } else { + frost::aggregate(signing_package, signature_shares, public_key_package) + } +} + +/// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256). +pub type SigningKey = frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(secp256k1, SHA-256). +pub type VerifyingKey = frost_core::VerifyingKey; diff --git a/frost-secp256k1-tr/src/tests.rs b/frost-secp256k1-tr/src/tests.rs new file mode 100644 index 0000000..15a3e18 --- /dev/null +++ b/frost-secp256k1-tr/src/tests.rs @@ -0,0 +1,5 @@ +mod batch; +mod coefficient_commitment; +mod deserialize; +mod proptests; +mod vss_commitment; diff --git a/frost-secp256k1-tr/src/tests/batch.rs b/frost-secp256k1-tr/src/tests/batch.rs new file mode 100644 index 0000000..f88793a --- /dev/null +++ b/frost-secp256k1-tr/src/tests/batch.rs @@ -0,0 +1,24 @@ +use rand::thread_rng; + +use crate::*; + +#[test] +fn check_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::batch_verify::(rng); +} + +#[test] +fn check_bad_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::bad_batch_verify::(rng); +} + +#[test] +fn empty_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::empty_batch_verify::(rng); +} diff --git a/frost-secp256k1-tr/src/tests/coefficient_commitment.rs b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs new file mode 100644 index 0000000..a63259c --- /dev/null +++ b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs @@ -0,0 +1,46 @@ +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization of CoefficientCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialization_of_coefficient_commitment() { + let rng = thread_rng(); + frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_create_coefficient_commitment() { + let rng = thread_rng(); + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} +#[test] +fn check_create_coefficient_commitment_error() { + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment_error::< + Secp256K1Sha256TR, + >(&ELEMENTS); +} + +#[test] +fn check_get_value_of_coefficient_commitment() { + let rng = thread_rng(); + + frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} diff --git a/frost-secp256k1-tr/src/tests/deserialize.rs b/frost-secp256k1-tr/src/tests/deserialize.rs new file mode 100644 index 0000000..7d4c630 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/deserialize.rs @@ -0,0 +1,38 @@ +use crate::*; + +#[test] +fn check_deserialize_non_canonical() { + let mut encoded_generator = ::Group::serialize( + &::Group::generator(), + ) + .unwrap(); + + let r = ::Group::deserialize(&encoded_generator); + assert!(r.is_ok()); + + // The first byte should be 0x02 or 0x03. Set other value to + // create a non-canonical encoding. + encoded_generator[0] = 0xFF; + let r = ::Group::deserialize(&encoded_generator); + assert_eq!(r, Err(GroupError::MalformedElement)); + + // Besides the first byte, it is still possible to get non-canonical encodings. + // This is x = p + 2 which is non-canonical and maps to a valid prime-order point. + let encoded_point = + hex::decode("02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc31") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::MalformedElement)); +} + +#[test] +fn check_deserialize_identity() { + // The identity is actually encoded as a single byte; but the API does not + // allow us to change that. Try to send something similar. + let encoded_identity = [0u8; 33]; + + let r = ::Group::deserialize(&encoded_identity); + assert_eq!(r, Err(GroupError::MalformedElement)); +} diff --git a/frost-secp256k1-tr/src/tests/proptests.rs b/frost-secp256k1-tr/src/tests/proptests.rs new file mode 100644 index 0000000..cad88c3 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/proptests.rs @@ -0,0 +1,33 @@ +use crate::*; +use frost_core::tests::proptests::{tweak_strategy, SignatureCase}; +use proptest::prelude::*; + +use rand_chacha::ChaChaRng; +use rand_core::SeedableRng; + +proptest! { + + #[test] + fn tweak_signature( + tweaks in prop::collection::vec(tweak_strategy(), (0,5)), + rng_seed in prop::array::uniform32(any::()), + ) { + // Use a deterministic RNG so that test failures can be reproduced. + // Seeding with 64 bits of entropy is INSECURE and this code should + // not be copied outside of this test! + let rng = ChaChaRng::from_seed(rng_seed); + + // Create a test case for each signature type. + let msg = b"test message for proptests"; + let mut sig = SignatureCase::::new(rng, msg.to_vec()); + + // Apply tweaks to each case. + for t in &tweaks { + sig.apply_tweak(t); + } + + assert!(sig.check()); + } + + +} diff --git a/frost-secp256k1-tr/src/tests/vss_commitment.rs b/frost-secp256k1-tr/src/tests/vss_commitment.rs new file mode 100644 index 0000000..80fb1ca --- /dev/null +++ b/frost-secp256k1-tr/src/tests/vss_commitment.rs @@ -0,0 +1,42 @@ +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization VerifiableSecretSharingCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialize_vss_commitment() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); +} + +#[test] +fn check_deserialize_vss_commitment() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_deserialize_vss_commitment::( + rng, + ); +} + +#[test] +fn check_deserialize_vss_commitment_error() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( + rng, &ELEMENTS, + ); +} + +#[test] +fn check_compute_public_key_package() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_compute_public_key_package::( + rng, + ); +} diff --git a/frost-secp256k1-tr/tests/common_traits_tests.rs b/frost-secp256k1-tr/tests/common_traits_tests.rs new file mode 100644 index 0000000..81b97a9 --- /dev/null +++ b/frost-secp256k1-tr/tests/common_traits_tests.rs @@ -0,0 +1,74 @@ +#![cfg(feature = "serde")] + +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) { + // Make sure can be debug-printed. This also catches if the Debug does not + // have an endless recursion (a popular mistake). + println!("{:?}", v); + // Test Clone and Eq + assert_eq!(v, v.clone()); + // Make sure it can be unwrapped in a Result (which requires Debug). + let e: Result = Ok(v.clone()); + assert_eq!(v, e.unwrap()); +} + +#[test] +fn check_signing_key_common_traits() { + let mut rng = thread_rng(); + let signing_key = SigningKey::new(&mut rng); + check_common_traits_for_type(signing_key); +} + +#[test] +fn check_signing_commitments_common_traits() { + let commitments = samples::signing_commitments(); + check_common_traits_for_type(commitments); +} + +#[test] +fn check_signing_package_common_traits() { + let signing_package = samples::signing_package(); + check_common_traits_for_type(signing_package); +} + +#[test] +fn check_signature_share_common_traits() { + let signature_share = samples::signature_share(); + check_common_traits_for_type(signature_share); +} + +#[test] +fn check_secret_share_common_traits() { + let secret_share = samples::secret_share(); + check_common_traits_for_type(secret_share); +} + +#[test] +fn check_key_package_common_traits() { + let key_package = samples::key_package(); + check_common_traits_for_type(key_package); +} + +#[test] +fn check_public_key_package_common_traits() { + let public_key_package = samples::public_key_package(); + check_common_traits_for_type(public_key_package); +} + +#[test] +fn check_round1_package_common_traits() { + let round1_package = samples::round1_package(); + check_common_traits_for_type(round1_package); +} + +#[test] +fn check_round2_package_common_traits() { + let round2_package = samples::round2_package(); + check_common_traits_for_type(round2_package); +} diff --git a/frost-secp256k1-tr/tests/helpers/elements.json b/frost-secp256k1-tr/tests/helpers/elements.json new file mode 100644 index 0000000..e8cd408 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/elements.json @@ -0,0 +1,5 @@ +{ + "elements": { + "invalid_element": "123456afdf4a7f88885ab26b20d18edb7d4d9589812a6cf1a5a1a09d3808dae5d8" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/mod.rs b/frost-secp256k1-tr/tests/helpers/mod.rs new file mode 100644 index 0000000..0de6147 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/mod.rs @@ -0,0 +1,24 @@ +// Required since each integration test is compiled as a separated crate, +// and each one uses only part of the module. +#![allow(dead_code)] + +use frost_secp256k1_tr::Secp256K1Sha256TR; +use secp256k1::Secp256k1; + +pub mod samples; + +pub fn verify_signature( + msg: &[u8], + group_signature: &frost_core::Signature, + group_pubkey: &frost_core::VerifyingKey, +) { + let secp = Secp256k1::new(); + let sig = secp256k1::schnorr::Signature::from_byte_array( + group_signature.serialize().unwrap().try_into().unwrap(), + ); + let pubkey = secp256k1::XOnlyPublicKey::from_byte_array( + &group_pubkey.serialize().unwrap()[1..33].try_into().unwrap(), + ) + .unwrap(); + secp.verify_schnorr(&sig, msg, &pubkey).unwrap(); +} diff --git a/frost-secp256k1-tr/tests/helpers/repair-share.json b/frost-secp256k1-tr/tests/helpers/repair-share.json new file mode 100644 index 0000000..f4db9bc --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/repair-share.json @@ -0,0 +1,15 @@ +{ + "scalar_generation": { + "random_scalar_1": "1847f6c4a85096e5dbc9e200c9691c5164f8e276d32d4a54ebaf4275474a1403", + "random_scalar_2": "eac5595269d108812eaa865bf62c703a2c128a61fa3bd4dc837b9314bc515204", + "random_scalar_3": "5b3b6084e41c273a39a8d9bbbd87fbcd626c07030142bf78c6c91247bf175700", + "random_scalar_sum": "5e48b09bf63dc6a1441d42187d1d885a38c896f51f633e6e76218944f27c7bc6" + }, + "sigma_generation": { + "sigma_1": "ec3aa83140065181d75b746bfd6bbbbaf212bdfbb3a91670f924d1ca899cbc0c", + "sigma_2": "5dd288d659e0a2dd3ef7523a9cc4f80f4a7f919e9980005c7fbec0961d3fb500", + "sigma_3": "3e62e7461db9ca1ed2f1549a8114bbc87fa9242ce0012ed3f9ac9dcf23f4c30a", + "sigma_4": "684c44e7aba416a1982a8db8ec2a3095f5cc6a3f958a4716b69ae76524dd7200", + "sigma_sum": "f0bc5d356344d51f816ea8fa076fa029f7590120136bec7c6958b9081f7864d5" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.json b/frost-secp256k1-tr/tests/helpers/samples.json new file mode 100644 index 0000000..1cc174a --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.json @@ -0,0 +1,7 @@ +{ + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.rs b/frost-secp256k1-tr/tests/helpers/samples.rs new file mode 100644 index 0000000..11ef99e --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.rs @@ -0,0 +1,128 @@ +//! Generate sample, fixed instances of structs for testing. + +use std::collections::BTreeMap; + +use frost_core::{round1::Nonce, Ciphersuite, Element, Group, Scalar}; +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, SigningShare, VerifiableSecretSharingCommitment, + VerifyingShare, + }, + round1::{NonceCommitment, SigningCommitments, SigningNonces}, + round2::SignatureShare, + Field, Signature, SigningPackage, VerifyingKey, +}; + +type C = frost_secp256k1_tr::Secp256K1Sha256TR; + +fn element1() -> Element { + ::Group::generator() +} + +fn element2() -> Element { + element1() + element1() +} + +fn scalar1() -> Scalar { + let one = <::Group as Group>::Field::one(); + let three = one + one + one; + // To return a fixed non-small number, get the inverse of 3 + <::Group as Group>::Field::invert(&three) + .expect("nonzero elements have inverses") +} + +/// Generate a sample SigningCommitments. +pub fn signing_nonces() -> SigningNonces { + let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); + + SigningNonces::from_nonces(hiding_nonce, binding_nonce) +} + +/// Generate a sample SigningCommitments. +pub fn signing_commitments() -> SigningCommitments { + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); + + SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) +} + +/// Generate a sample SigningPackage. +pub fn signing_package() -> SigningPackage { + let identifier = 42u16.try_into().unwrap(); + let commitments = BTreeMap::from([(identifier, signing_commitments())]); + let message = "hello world".as_bytes(); + + SigningPackage::new(commitments, message) +} + +/// Generate a sample SignatureShare. +pub fn signature_share() -> SignatureShare { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() +} + +/// Generate a sample SecretShare. +pub fn secret_share() -> SecretShare { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + SecretShare::new(identifier, signing_share, vss_commitment) +} + +/// Generate a sample KeyPackage. +pub fn key_package() -> KeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + + KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) +} + +/// Generate a sample PublicKeyPackage. +pub fn public_key_package() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key) +} + +/// Generate a sample round1::Package. +pub fn round1_package() -> round1::Package { + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::Package::new(vss_commitment, signature) +} + +/// Generate a sample round2::Package. +pub fn round2_package() -> round2::Package { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + + round2::Package::new(signing_share) +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json new file mode 100644 index 0000000..2fedbec --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors.json b/frost-secp256k1-tr/tests/helpers/vectors.json new file mode 100644 index 0000000..2fedbec --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors_dkg.json b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json new file mode 100644 index 0000000..9fcbc83 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json @@ -0,0 +1,41 @@ +{ + "config": { + "MAX_PARTICIPANTS": 3, + "MIN_PARTICIPANTS": 2, + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "verifying_key": "03849089de77b56bd35fcbfc70bf38e73448131090acc75d538a5cea63cc3dcefe", + "1": { + "identifier": 1, + "signing_key": "68e3f6904c6043973515a36bf7801a71597da35733f21305d75a5234f06e4529", + "coefficient": "25d2d840a3e2718a431ec69e14ee8a015b000d43c7a9868060f01d5aa52a19d1", + "vss_commitments": ["03e7ba4acb164d2bd5eba4f47b3a788109ddb3f88f1181792424fa332123a25ea8", "037495e920a1f032916193aa80ea97a4c3a611dec9ab47ccc969deb664f5f88bbe"], + "proof_of_knowledge": "6689a8d414eb4961308e21f8caa1045236efded4f3de9209dc07547e88be3b42e192de9bed27fb78a7a4d4e35a0422f11f52631b8e66d69e609398eaff2770b8", + "signing_shares": { + "2": "1dd3cb3e2370e6af22917415f0ad584514807b58b3cc40d2230a26e115f02771", + "3": "dd25ee86acd01f996618aa0d1153f5e8fbc929a8e8a18b8f0a15f91d087217e2" + }, + "verifying_share": "02a8bf413b5d7af0e692fba967540cde8009f161a4d721f8c88649c1933bbb7531", + "signing_share": "f1be455a8ec9ab86ef8438f23a5cfdf70153aa2785d4bebba83e0840403e4bf3" + }, + "2": { + "identifier": 2, + "signing_key": "2619be8223b23e0453ddc630a4d164e81f7d8a9e07af33c4d4d02190df8bec13", + "coefficient": "f7ba0cbbffbea8aaceb3ade54bdbf35bafb1cda15b65ad490e0c63dd069a7c9f", + "vss_commitments": ["03ef10370a008cd95e179dc51e2cb7828f30b72d254e5166484f927c84ab326582", "022ce0dac0db217ba326fbbe3e6132d45e2a4bfa0a0c3790d91eacce9a1c2d6a10"], + "proof_of_knowledge": "a319dd51cf64b3896c22f54154812d4ae76cfa95f46f53ef69241fd702456fef32da76cc93d3a541ca495b723e793ee90c32440da5f314e2e58a2dc30550314a", + "verifying_share": "029ecb3a4db28a82e7b8d600d42711b02790dde3f063f0ecec6f812c1c5d7dcefc" + }, + "3": { + "identifier": 3, + "signing_key": "9a267f4cde8087a6eca0969425846209b41b515b73195ebbeeef8a991103f1ec", + "coefficient": "42ff6f39ce4f97f279781378ebcf93df47add84d75882cd31b266e83f76e25f6", + "vss_commitments": ["02da186c3863c5600b471a2799cb6f15ae4d8315a2f225c177798880e75ac820a0", "03e6a36e7fa4b117c1aa428886672e3a35d926bb4c585a9b07d8ee9a3387420067"], + "proof_of_knowledge": "6e115d9e63fd15d432b380ccf1ec4ed03340fcf96caeae8985aedb5f905b1a65dc422ffe5878988fbbc55454857736c7755d9c8f5ee6822c8833ea21d54dba36", + "verifying_share": "02c98b3c2e9f4bde4cf90dc9c7be639e5adda6ea09fc605239880a22cb836f7145" + } + } +} diff --git a/frost-secp256k1-tr/tests/integration_tests.rs b/frost-secp256k1-tr/tests/integration_tests.rs new file mode 100644 index 0000000..9187285 --- /dev/null +++ b/frost-secp256k1-tr/tests/integration_tests.rs @@ -0,0 +1,359 @@ +use frost_secp256k1_tr::*; +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +#[test] +fn check_zero_key_fails() { + frost_core::tests::ciphersuite_generic::check_zero_key_fails::(); +} + +#[test] +fn check_sign_with_dkg() { + let rng = thread_rng(); + + 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 min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_invalid_signers_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_rts() { + let rng = thread_rng(); + + frost_core::tests::repairable::check_rts::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_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 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); +} + +#[test] +fn check_sign_with_dealer() { + let rng = thread_rng(); + + 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 min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_invalid_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +/// This is testing that Shamir's secret sharing to compute and arbitrary +/// value is working. +#[test] +fn check_share_generation_secp256k1_tr_sha256() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_share_generation::(rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_min_signers() { + let rng = thread_rng(); + + let min_signers = 0; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 0; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +lazy_static! { + pub static ref VECTORS: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_BIG_IDENTIFIER: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors-big-identifier.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_DKG: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors_dkg.json").trim()) + .expect("Test vector is valid JSON"); +} + +#[test] +fn check_sign_with_test_vectors() { + frost_core::tests::vectors::check_sign_with_test_vectors::(&VECTORS); +} + +#[test] +fn check_sign_with_test_vectors_dkg() { + frost_core::tests::vectors_dkg::check_dkg_keygen::(&VECTORS_DKG); +} + +#[test] +fn check_sign_with_test_vectors_with_big_identifiers() { + frost_core::tests::vectors::check_sign_with_test_vectors::( + &VECTORS_BIG_IDENTIFIER, + ); +} + +#[test] +fn check_error_culprit() { + frost_core::tests::ciphersuite_generic::check_error_culprit::(); +} + +#[test] +fn check_identifier_derivation() { + frost_core::tests::ciphersuite_generic::check_identifier_derivation::(); +} + +// Explicit test which is used in a documentation snippet +#[test] +#[allow(unused_variables)] +fn check_identifier_generation() -> Result<(), Error> { + // ANCHOR: dkg_identifier + let participant_identifier = Identifier::try_from(7u16)?; + let participant_identifier = Identifier::derive("alice@example.com".as_bytes())?; + // ANCHOR_END: dkg_identifier + Ok(()) +} + +#[test] +fn check_sign_with_dealer_and_identifiers() { + let rng = thread_rng(); + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_missing_identifier() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_incorrect_commitments() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::< + Secp256K1Sha256TR, + _, + >(rng); +} diff --git a/frost-secp256k1-tr/tests/interoperability_tests.rs b/frost-secp256k1-tr/tests/interoperability_tests.rs new file mode 100644 index 0000000..b2e3f9a --- /dev/null +++ b/frost-secp256k1-tr/tests/interoperability_tests.rs @@ -0,0 +1,53 @@ +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(); + + 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"); + helpers::verify_signature(b"message", &signature, &verifying_key); + } +} + +#[test] +fn check_interoperability_in_sign_with_dkg() { + let rng = thread_rng(); + + // Test with multiple keys/signatures to better exercise the key generation + // and the interoperability check. A smaller number of iterations is used + // because DKG takes longer and otherwise the test would be too slow. + for _ in 0..32 { + let (message, group_signature, group_pubkey) = + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::( + rng.clone(), + ); + + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} + +#[test] +fn check_interoperability_in_sign_with_dealer() { + let rng = thread_rng(); + + // 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(), + ); + + // Check that the threshold signature can be verified by the `ed25519_dalek` crate + // public key (interoperability test) + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} diff --git a/frost-secp256k1-tr/tests/recreation_tests.rs b/frost-secp256k1-tr/tests/recreation_tests.rs new file mode 100644 index 0000000..477de37 --- /dev/null +++ b/frost-secp256k1-tr/tests/recreation_tests.rs @@ -0,0 +1,133 @@ +//! Test for recreating packages from their components, which shows that they +//! can be serialized and deserialized as the user wishes. + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +mod helpers; + +use helpers::samples; + +/// Check if SigningNonces can be recreated. +#[test] +fn check_signing_nonces_recreation() { + let nonces = samples::signing_nonces(); + let hiding = nonces.hiding(); + let binding = nonces.binding(); + let new_nonces = SigningNonces::from_nonces(*hiding, *binding); + assert!(nonces == new_nonces); +} + +/// Check if SigningCommitments can be recreated. +#[test] +fn check_signing_commitments_recreation() { + let commitments = samples::signing_commitments(); + let hiding = commitments.hiding(); + let binding = commitments.binding(); + let new_commitments = SigningCommitments::new(*hiding, *binding); + assert!(commitments == new_commitments); +} + +/// Check if SigningPackage can be recreated. +#[test] +fn check_signing_package_recreation() { + let signing_package = samples::signing_package(); + + let commitments = signing_package.signing_commitments(); + let message = signing_package.message(); + + let new_signing_package = SigningPackage::new(commitments.clone(), message); + assert!(signing_package == new_signing_package); +} + +/// Check if SignatureShare can be recreated. +#[test] +fn check_signature_share_recreation() { + let signature_share = samples::signature_share(); + + let encoded = signature_share.serialize(); + + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); + assert!(signature_share == new_signature_share); +} + +/// Check if SecretShare can be recreated. +#[test] +fn check_secret_share_recreation() { + let secret_share = samples::secret_share(); + + let identifier = secret_share.identifier(); + let value = secret_share.signing_share(); + let commitment = secret_share.commitment(); + + let new_secret_share = SecretShare::new(*identifier, *value, commitment.clone()); + + assert!(secret_share == new_secret_share); +} + +/// Check if KeyPackage can be recreated. +#[test] +fn check_key_package_recreation() { + let key_package = samples::key_package(); + + let identifier = key_package.identifier(); + let signing_share = key_package.signing_share(); + let verifying_share = key_package.verifying_share(); + let verifying_key = key_package.verifying_key(); + let min_signers = key_package.min_signers(); + + let new_key_package = KeyPackage::new( + *identifier, + *signing_share, + *verifying_share, + *verifying_key, + *min_signers, + ); + + assert!(key_package == new_key_package); +} + +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_recreation() { + let public_key_package = samples::public_key_package(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + + let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::Package can be recreated. +#[test] +fn check_round1_package_recreation() { + let round1_package = samples::round1_package(); + + let vss_commitment = round1_package.commitment(); + let signature = round1_package.proof_of_knowledge(); + + let new_round1_package = round1::Package::new(vss_commitment.clone(), *signature); + + assert!(round1_package == new_round1_package); +} + +/// Check if round2::Package can be recreated. +#[test] +fn check_round2_package_recreation() { + let round2_package = samples::round2_package(); + + let signing_share = round2_package.signing_share(); + + let new_round2_package = round2::Package::new(*signing_share); + + assert!(round2_package == new_round2_package); +} diff --git a/frost-secp256k1-tr/tests/rerandomized_tests.rs b/frost-secp256k1-tr/tests/rerandomized_tests.rs new file mode 100644 index 0000000..67e1431 --- /dev/null +++ b/frost-secp256k1-tr/tests/rerandomized_tests.rs @@ -0,0 +1,10 @@ +use frost_secp256k1_tr::Secp256K1Sha256TR; +use rand::thread_rng; + +#[test] +fn check_randomized_sign_with_dealer() { + let rng = thread_rng(); + + let (_msg, _group_signature, _group_pubkey) = + frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); +} diff --git a/frost-secp256k1-tr/tests/serde_tests.rs b/frost-secp256k1-tr/tests/serde_tests.rs new file mode 100644 index 0000000..62a70e7 --- /dev/null +++ b/frost-secp256k1-tr/tests/serde_tests.rs @@ -0,0 +1,632 @@ +#![cfg(feature = "serde")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::SigningCommitments, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; + +#[test] +fn check_signing_commitments_serialization() { + let commitments = samples::signing_commitments(); + + let json = serde_json::to_string_pretty(&commitments).unwrap(); + println!("{}", json); + + let decoded_commitments: SigningCommitments = serde_json::from_str(&json).unwrap(); + assert!(commitments == decoded_commitments); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + let decoded_commitments: SigningCommitments = serde_json::from_str(json).unwrap(); + assert!(commitments == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Wrong ciphersuite + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Wrong, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0000000000000000000000000000000000000000000000000000000000000000", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Ed25519, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signing_package_serialization() { + let signing_package = samples::signing_package(); + + let json = serde_json::to_string_pretty(&signing_package).unwrap(); + println!("{}", json); + + let decoded_signing_package: SigningPackage = serde_json::from_str(&json).unwrap(); + assert!(signing_package == decoded_signing_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + let decoded_signing_package: SigningPackage = serde_json::from_str(json).unwrap(); + assert!(signing_package == decoded_signing_package); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "0000000000000000000000000000000000000000000000000000000000000000": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64", + "extra": 1 + } + "#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signature_share_serialization() { + let signature_share = samples::signature_share(); + + let json = serde_json::to_string_pretty(&signature_share).unwrap(); + println!("{}", json); + + let decoded_signature_share: SignatureShare = serde_json::from_str(&json).unwrap(); + assert!(signature_share == decoded_signature_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_commitments: SignatureShare = serde_json::from_str(json).unwrap(); + assert!(signature_share == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_secret_share_serialization() { + let secret_share = samples::secret_share(); + + let json = serde_json::to_string_pretty(&secret_share).unwrap(); + println!("{}", json); + + let decoded_secret_share: SecretShare = serde_json::from_str(&json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + let decoded_secret_share: SecretShare = serde_json::from_str(json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + "extra": 1, + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_key_package_serialization() { + let key_package = samples::key_package(); + + let json = serde_json::to_string_pretty(&key_package).unwrap(); + println!("{}", json); + + let decoded_key_package: KeyPackage = serde_json::from_str(&json).unwrap(); + assert!(key_package == decoded_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + let decoded_key_package: KeyPackage = serde_json::from_str(json).unwrap(); + assert!(key_package == decoded_key_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra_field": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid version + let invalid_json = r#"{ + "header": { + "version": 1, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "secret_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "group_public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_public_key_package_serialization() { + let public_key_package = samples::public_key_package(); + + let json = serde_json::to_string_pretty(&public_key_package).unwrap(); + println!("{}", json); + + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(&json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "0000000000000000000000000000000000000000000000000000000000000000": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round1_package_serialization() { + let round1_package = samples::round1_package(); + + let json = serde_json::to_string_pretty(&round1_package).unwrap(); + println!("{}", json); + + let decoded_round1_package: round1::Package = serde_json::from_str(&json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round1_package: round1::Package = serde_json::from_str(json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "foo": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round2_package_serialization() { + let round2_package = samples::round2_package(); + + let json = serde_json::to_string_pretty(&round2_package).unwrap(); + println!("{}", json); + + let decoded_round2_package: round2::Package = serde_json::from_str(&json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round2_package: round2::Package = serde_json::from_str(json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} diff --git a/frost-secp256k1-tr/tests/serialization_tests.rs b/frost-secp256k1-tr/tests/serialization_tests.rs new file mode 100644 index 0000000..8f93cfb --- /dev/null +++ b/frost-secp256k1-tr/tests/serialization_tests.rs @@ -0,0 +1,105 @@ +#![cfg(feature = "serialization")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; +use insta::assert_snapshot; + +#[test] +fn check_signing_nonces_postcard_serialization() { + let nonces = samples::signing_nonces(); + let bytes: Vec<_> = nonces.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(nonces, SigningNonces::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_signing_commitments_postcard_serialization() { + let commitments = samples::signing_commitments(); + let bytes: Vec<_> = commitments.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + commitments, + SigningCommitments::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signing_package_postcard_serialization() { + let signing_package = samples::signing_package(); + let bytes: Vec<_> = signing_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signing_package, + SigningPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signature_share_postcard_serialization() { + let signature_share = samples::signature_share(); + let bytes = signature_share.serialize(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); +} +#[test] +fn check_secret_share_postcard_serialization() { + let secret_share = samples::secret_share(); + let bytes: Vec<_> = secret_share.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(secret_share, SecretShare::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_key_package_postcard_serialization() { + let key_package = samples::key_package(); + let bytes: Vec<_> = key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(key_package, KeyPackage::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_public_key_package_postcard_serialization() { + let public_key_package = samples::public_key_package(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_package_postcard_serialization() { + let round1_package = samples::round1_package(); + let bytes: Vec<_> = round1_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_package, + round1::Package::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round2_package_postcard_serialization() { + let round2_package = samples::round2_package(); + let bytes: Vec<_> = round2_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_package, + round2::Package::deserialize(&bytes).unwrap() + ); +} diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap new file mode 100644 index 0000000..ca169f4 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b810279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap new file mode 100644 index 0000000..5600403 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap new file mode 100644 index 0000000..9099b14 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap new file mode 100644 index 0000000..218294f --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap new file mode 100644 index 0000000..82e3585 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap new file mode 100644 index 0000000..aa7a503 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(bytes)" +--- +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap new file mode 100644 index 0000000..66962d3 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap new file mode 100644 index 0000000..537b8e3 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b8100230f8ab3034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap new file mode 100644 index 0000000..b398e5e --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee50b68656c6c6f20776f726c64 diff --git a/frost-secp256k1-tr/tests/tweaking_tests.rs b/frost-secp256k1-tr/tests/tweaking_tests.rs new file mode 100644 index 0000000..3fc74ae --- /dev/null +++ b/frost-secp256k1-tr/tests/tweaking_tests.rs @@ -0,0 +1,149 @@ +use std::{error::Error, vec}; + +use k256::elliptic_curve::point::AffineCoordinates; +use k256::ProjectivePoint; +use keys::Tweak; +use sha2::{Digest, Sha256}; + +use frost_secp256k1_tr::*; + +mod helpers; + +#[test] +fn check_tweaked_sign_with_dealer() -> Result<(), Box> { + use frost_secp256k1_tr as frost; + use rand::thread_rng; + use std::collections::BTreeMap; + + let merkle_root: Vec = vec![12; 32]; + + let mut rng = thread_rng(); + 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, + )?; + let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + for (identifier, secret_share) in shares { + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + key_packages.insert(identifier, key_package); + } + + let mut nonces_map = BTreeMap::new(); + let mut commitments_map = BTreeMap::new(); + + for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), &mut rng); + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); + } + + let mut signature_shares = BTreeMap::new(); + let message = "message to sign".as_bytes(); + let signing_package = frost::SigningPackage::new(commitments_map, message); + + for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; + let signature_share = frost::round2::sign_with_tweak( + &signing_package, + nonces, + key_package, + Some(&merkle_root), + )?; + signature_shares.insert(*participant_identifier, signature_share); + } + + let group_signature = frost::aggregate_with_tweak( + &signing_package, + &signature_shares, + &pubkey_package, + Some(&merkle_root), + )?; + + pubkey_package + .verifying_key() + .verify(message, &group_signature) + .expect_err("signature should not be valid for untweaked pubkey_package"); + + let pubkey_package_tweaked = pubkey_package.clone().tweak(Some(&merkle_root)); + pubkey_package_tweaked + .verifying_key() + .verify(message, &group_signature) + .expect("signature should be valid for tweaked pubkey_package"); + + helpers::verify_signature( + message, + &group_signature, + pubkey_package_tweaked.verifying_key(), + ); + + // Confirm the internal (untweaked) group key can be provided to access + // script spending paths under the output (tweaked) group key. + let (expected_parity, expected_tr_output_pubkey) = taproot_tweak_pubkey( + pubkey_package + .verifying_key() + .to_element() + .to_affine() + .x() + .into(), + &merkle_root, + ); + + let tr_output_point = pubkey_package_tweaked + .verifying_key() + .to_element() + .to_affine(); + + let tr_output_pubkey: [u8; 32] = tr_output_point.x().into(); + let tr_output_parity: bool = tr_output_point.y_is_odd().into(); + + assert_eq!( + tr_output_pubkey, expected_tr_output_pubkey, + "taproot output pubkey does not match" + ); + + assert_eq!( + tr_output_parity, expected_parity, + "taproot output pubkey parity bit does not match" + ); + + Ok(()) +} + +/// Emulates the BIP341 helper function: +/// +/// def taproot_tweak_pubkey(pubkey, h): +/// t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)) +/// if t >= SECP256K1_ORDER: +/// raise ValueError +/// P = lift_x(int_from_bytes(pubkey)) +/// if P is None: +/// raise ValueError +/// Q = point_add(P, point_mul(G, t)) +/// return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q)) +/// +fn taproot_tweak_pubkey(pubkey: [u8; 32], merkle_root: &[u8]) -> (bool, [u8; 32]) { + let prefix = Sha256::digest(b"TapTweak"); + let tweak_hash = Sha256::new() + .chain_update(prefix) + .chain_update(prefix) + .chain_update(pubkey) + .chain_update(merkle_root) + .finalize(); + let t = k256::Scalar::from( + k256::elliptic_curve::ScalarPrimitive::new(k256::U256::from_be_slice(&tweak_hash)).unwrap(), + ); + + let mut pubkey_even_bytes = [0x02; 33]; + pubkey_even_bytes[1..].copy_from_slice(&pubkey); + let pubkey_even = Secp256K1Group::deserialize(&pubkey_even_bytes).unwrap(); + + let tr_output_key = (pubkey_even + ProjectivePoint::GENERATOR * t).to_affine(); + (tr_output_key.y_is_odd().into(), tr_output_key.x().into()) +} diff --git a/frost-secp256k1/Cargo.toml b/frost-secp256k1/Cargo.toml index cf3afc6..cdc8adc 100644 --- a/frost-secp256k1/Cargo.toml +++ b/frost-secp256k1/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "2.0.0-rc.0" +version = "2.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -23,16 +23,16 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +frost-core = { path = "../frost-core", version = "2.0.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", default-features = false } k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"], default-features = false } rand_core = "0.6" sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "2.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } lazy_static = "1.4" diff --git a/frost-secp256k1/dkg.md b/frost-secp256k1/dkg.md index ac0980b..5d62857 100644 --- a/frost-secp256k1/dkg.md +++ b/frost-secp256k1/dkg.md @@ -3,7 +3,7 @@ The DKG module supports generating FROST key shares in a distributed manner, without a trusted dealer. -Before starting, each participant needs an unique identifier, which can be built from +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 diff --git a/frost-secp256k1/src/lib.rs b/frost-secp256k1/src/lib.rs index 4d25266..903ffda 100644 --- a/frost-secp256k1/src/lib.rs +++ b/frost-secp256k1/src/lib.rs @@ -8,7 +8,6 @@ extern crate alloc; -use alloc::borrow::ToOwned; use alloc::collections::BTreeMap; use frost_rerandomized::RandomizedCiphersuite; @@ -159,9 +158,9 @@ fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { output } -fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { let mut u = [Secp256K1ScalarField::zero()]; - hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + hash_to_field::, Scalar>(&[msg], domain, &mut u) .expect("should never return error according to error cases described in ExpandMsgXmd"); u[0] } @@ -188,21 +187,21 @@ impl Ciphersuite for Secp256K1Sha256 { /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.2 fn H1(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) } /// H2 for FROST(secp256k1, SHA-256) /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.4 fn H2(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "chal").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"chal"], m) } /// H3 for FROST(secp256k1, SHA-256) /// /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#section-6.5-2.4.2.6 fn H3(m: &[u8]) -> <::Field as Field>::Scalar { - hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) } /// H4 for FROST(secp256k1, SHA-256) @@ -221,25 +220,19 @@ impl Ciphersuite for Secp256K1Sha256 { /// HDKG for FROST(secp256k1, SHA-256) fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "dkg").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) } /// HID for FROST(secp256k1, SHA-256) fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "id").as_bytes(), - m, - )) + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) } } impl RandomizedCiphersuite for Secp256K1Sha256 { fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { Some(hash_to_scalar( - (CONTEXT_STRING.to_owned() + "randomizer").as_bytes(), + &[CONTEXT_STRING.as_bytes(), b"randomizer"], m, )) } diff --git a/frost-secp256k1/tests/helpers/samples.json b/frost-secp256k1/tests/helpers/samples.json index 210c6f2..54f6e1e 100644 --- a/frost-secp256k1/tests/helpers/samples.json +++ b/frost-secp256k1/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" -} \ No newline at end of file +} diff --git a/frost-secp256k1/tests/helpers/samples.rs b/frost-secp256k1/tests/helpers/samples.rs index 3afe78f..11b8408 100644 --- a/frost-secp256k1/tests/helpers/samples.rs +++ b/frost-secp256k1/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/gencode/src/main.rs b/gencode/src/main.rs index eda52d4..8a420fc 100644 --- a/gencode/src/main.rs +++ b/gencode/src/main.rs @@ -121,17 +121,14 @@ fn write_docs( // To be able to replace the documentation properly, start from the end, which // will keep the string positions consistent for (old_name, _, old_start, old_end) in old_docs.iter().rev() { - let new_doc = docs - .get(old_name) - .unwrap_or_else(|| { - panic!( - "documentation for {} is not available in base file", - old_name - ) - }) - .1 - .clone(); - + let new_doc = docs.get(old_name).map(|v| v.1.clone()); + let Some(new_doc) = new_doc else { + eprintln!( + "WARNING: documentation for {} is not available in base file. This can mean it's a specific type for the ciphersuite, or that there is a bug in gencode", + old_name + ); + continue; + }; // Replaces ciphersuite-references in documentation let mut new_doc = new_doc.to_string(); for (old_n, new_n) in zip(original_suite_strings.iter(), new_suite_strings.iter()) { @@ -227,7 +224,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{original_folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { original_strings.push(samples[key].as_str().unwrap().to_owned()); } let original_strings: Vec<&str> = original_strings.iter().map(|s| s.as_ref()).collect(); @@ -290,6 +293,19 @@ fn main() -> ExitCode { "", ], ), + ( + "frost-secp256k1-tr", + &[ + "Secp256K1Sha256TR", + "secp256k1 curve (Taproot)", + "Secp256K1", + "FROST(secp256k1, SHA-256)", + "FROST-secp256k1-SHA256-TR-v1", + "secp256k1_tr_sha256", + "secp256k1_tr", + "", + ], + ), ] { // Some test use "sample" values. To make these tests work for another ciphersuites, // these values must be replaced. To make it cleaner, the strings are @@ -300,7 +316,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { replacement_strings.push(samples[key].as_str().unwrap().to_owned()); } let replacement_strings: Vec<&str> =