diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 823ca01a..6cbfe3ed 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ concurrency: on: workflow_dispatch: pull_request: - branches: + branches: - main paths: # doc source files @@ -20,9 +20,9 @@ on: # workflow definitions - '.github/workflows/docs.yml' push: - branches: + branches: - main - + env: RUST_LOG: info RUST_BACKTRACE: full @@ -62,14 +62,15 @@ jobs: mdbook-version: '0.4.18' # TODO: actions-mdbook does not yet have an option to install mdbook-mermaid https://github.com/peaceiris/actions-mdbook/issues/426 - - name: Install mdbook + - name: Install plugins run: | cargo install mdbook-mermaid - + cargo install mdbook-admonish + - name: Build FROST book run: | mdbook build book/ - + - name: Deploy FROST book to Firebase preview channel uses: FirebaseExtended/action-hosting-deploy@v0 if: ${{ github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' }} diff --git a/README.md b/README.md index fa2be595..74966229 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FROST (Flexible Round-Optimised Schnorr Threshold signatures) +# ZF FROST (Flexible Round-Optimised Schnorr Threshold signatures) Rust implementations of ['Two-Round Threshold Schnorr Signatures with FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/). @@ -17,6 +17,10 @@ to compute a signature, and implements signing efficiency improvements described [Schnorr21](https://eprint.iacr.org/2021/1375.pdf). Single-round signing with FROST is not implemented here. +## Getting Started + +Refer to the [ZF FROST book](https://frost.zfnd.org/). + ## Status ⚠ The FROST specification is not yet finalized, and this codebase has not yet been audited or diff --git a/book/book.toml b/book/book.toml index 31f13849..b0e302a4 100644 --- a/book/book.toml +++ b/book/book.toml @@ -3,4 +3,15 @@ authors = ["Zcash Foundation "] language = "en" multilingual = false src = "src" -title = "The FROST Book" +title = "The ZF FROST Book" + +[preprocessor] + +[preprocessor.admonish] +command = "mdbook-admonish" +assets_version = "2.0.1" # do not edit: managed by `mdbook-admonish install` + +[output] + +[output.html] +additional-css = ["./mdbook-admonish.css"] diff --git a/book/mdbook-admonish.css b/book/mdbook-admonish.css new file mode 100644 index 00000000..244bc9ad --- /dev/null +++ b/book/mdbook-admonish.css @@ -0,0 +1,353 @@ +@charset "UTF-8"; +:root { + --md-admonition-icon--note: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--abstract: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--info: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--tip: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--success: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--question: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--warning: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--failure: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--danger: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--bug: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--example: + url("data:image/svg+xml;charset=utf-8,"); + --md-admonition-icon--quote: + url("data:image/svg+xml;charset=utf-8,"); + --md-details-icon: + url("data:image/svg+xml;charset=utf-8,"); +} + +:is(.admonition) { + display: flow-root; + margin: 1.5625em 0; + padding: 0 1.2rem; + color: var(--fg); + page-break-inside: avoid; + background-color: var(--bg); + border: 0 solid black; + border-inline-start-width: 0.4rem; + border-radius: 0.2rem; + box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1); +} +@media print { + :is(.admonition) { + box-shadow: none; + } +} +:is(.admonition) > * { + box-sizing: border-box; +} +:is(.admonition) :is(.admonition) { + margin-top: 1em; + margin-bottom: 1em; +} +:is(.admonition) > .tabbed-set:only-child { + margin-top: 0; +} +html :is(.admonition) > :last-child { + margin-bottom: 1.2rem; +} + +a.admonition-anchor-link { + display: none; + position: absolute; + left: -1.2rem; + padding-right: 1rem; +} +a.admonition-anchor-link:link, a.admonition-anchor-link:visited { + color: var(--fg); +} +a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover { + text-decoration: none; +} +a.admonition-anchor-link::before { + content: "§"; +} + +:is(.admonition-title, summary) { + position: relative; + min-height: 4rem; + margin-block: 0; + margin-inline: -1.6rem -1.2rem; + padding-block: 0.8rem; + padding-inline: 4.4rem 1.2rem; + font-weight: 700; + background-color: rgba(68, 138, 255, 0.1); + display: flex; +} +:is(.admonition-title, summary) p { + margin: 0; +} +html :is(.admonition-title, summary):last-child { + margin-bottom: 0; +} +:is(.admonition-title, summary)::before { + position: absolute; + top: 0.625em; + inset-inline-start: 1.6rem; + width: 2rem; + height: 2rem; + background-color: #448aff; + mask-image: url('data:image/svg+xml;charset=utf-8,'); + -webkit-mask-image: url('data:image/svg+xml;charset=utf-8,'); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-size: contain; + content: ""; +} +:is(.admonition-title, summary):hover a.admonition-anchor-link { + display: initial; +} + +details.admonition > summary.admonition-title::after { + position: absolute; + top: 0.625em; + inset-inline-end: 1.6rem; + height: 2rem; + width: 2rem; + background-color: currentcolor; + mask-image: var(--md-details-icon); + -webkit-mask-image: var(--md-details-icon); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-size: contain; + content: ""; + transform: rotate(0deg); + transition: transform 0.25s; +} +details[open].admonition > summary.admonition-title::after { + transform: rotate(90deg); +} + +:is(.admonition):is(.note) { + border-color: #448aff; +} + +:is(.note) > :is(.admonition-title, summary) { + background-color: rgba(68, 138, 255, 0.1); +} +:is(.note) > :is(.admonition-title, summary)::before { + background-color: #448aff; + mask-image: var(--md-admonition-icon--note); + -webkit-mask-image: var(--md-admonition-icon--note); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.abstract, .summary, .tldr) { + border-color: #00b0ff; +} + +:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) { + background-color: rgba(0, 176, 255, 0.1); +} +:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary)::before { + background-color: #00b0ff; + mask-image: var(--md-admonition-icon--abstract); + -webkit-mask-image: var(--md-admonition-icon--abstract); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.info, .todo) { + border-color: #00b8d4; +} + +:is(.info, .todo) > :is(.admonition-title, summary) { + background-color: rgba(0, 184, 212, 0.1); +} +:is(.info, .todo) > :is(.admonition-title, summary)::before { + background-color: #00b8d4; + mask-image: var(--md-admonition-icon--info); + -webkit-mask-image: var(--md-admonition-icon--info); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.tip, .hint, .important) { + border-color: #00bfa5; +} + +:is(.tip, .hint, .important) > :is(.admonition-title, summary) { + background-color: rgba(0, 191, 165, 0.1); +} +:is(.tip, .hint, .important) > :is(.admonition-title, summary)::before { + background-color: #00bfa5; + mask-image: var(--md-admonition-icon--tip); + -webkit-mask-image: var(--md-admonition-icon--tip); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.success, .check, .done) { + border-color: #00c853; +} + +:is(.success, .check, .done) > :is(.admonition-title, summary) { + background-color: rgba(0, 200, 83, 0.1); +} +:is(.success, .check, .done) > :is(.admonition-title, summary)::before { + background-color: #00c853; + mask-image: var(--md-admonition-icon--success); + -webkit-mask-image: var(--md-admonition-icon--success); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.question, .help, .faq) { + border-color: #64dd17; +} + +:is(.question, .help, .faq) > :is(.admonition-title, summary) { + background-color: rgba(100, 221, 23, 0.1); +} +:is(.question, .help, .faq) > :is(.admonition-title, summary)::before { + background-color: #64dd17; + mask-image: var(--md-admonition-icon--question); + -webkit-mask-image: var(--md-admonition-icon--question); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.warning, .caution, .attention) { + border-color: #ff9100; +} + +:is(.warning, .caution, .attention) > :is(.admonition-title, summary) { + background-color: rgba(255, 145, 0, 0.1); +} +:is(.warning, .caution, .attention) > :is(.admonition-title, summary)::before { + background-color: #ff9100; + mask-image: var(--md-admonition-icon--warning); + -webkit-mask-image: var(--md-admonition-icon--warning); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.failure, .fail, .missing) { + border-color: #ff5252; +} + +:is(.failure, .fail, .missing) > :is(.admonition-title, summary) { + background-color: rgba(255, 82, 82, 0.1); +} +:is(.failure, .fail, .missing) > :is(.admonition-title, summary)::before { + background-color: #ff5252; + mask-image: var(--md-admonition-icon--failure); + -webkit-mask-image: var(--md-admonition-icon--failure); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.danger, .error) { + border-color: #ff1744; +} + +:is(.danger, .error) > :is(.admonition-title, summary) { + background-color: rgba(255, 23, 68, 0.1); +} +:is(.danger, .error) > :is(.admonition-title, summary)::before { + background-color: #ff1744; + mask-image: var(--md-admonition-icon--danger); + -webkit-mask-image: var(--md-admonition-icon--danger); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.bug) { + border-color: #f50057; +} + +:is(.bug) > :is(.admonition-title, summary) { + background-color: rgba(245, 0, 87, 0.1); +} +:is(.bug) > :is(.admonition-title, summary)::before { + background-color: #f50057; + mask-image: var(--md-admonition-icon--bug); + -webkit-mask-image: var(--md-admonition-icon--bug); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.example) { + border-color: #7c4dff; +} + +:is(.example) > :is(.admonition-title, summary) { + background-color: rgba(124, 77, 255, 0.1); +} +:is(.example) > :is(.admonition-title, summary)::before { + background-color: #7c4dff; + mask-image: var(--md-admonition-icon--example); + -webkit-mask-image: var(--md-admonition-icon--example); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +:is(.admonition):is(.quote, .cite) { + border-color: #9e9e9e; +} + +:is(.quote, .cite) > :is(.admonition-title, summary) { + background-color: rgba(158, 158, 158, 0.1); +} +:is(.quote, .cite) > :is(.admonition-title, summary)::before { + background-color: #9e9e9e; + mask-image: var(--md-admonition-icon--quote); + -webkit-mask-image: var(--md-admonition-icon--quote); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-repeat: no-repeat; +} + +.navy :is(.admonition) { + background-color: var(--sidebar-bg); +} + +.ayu :is(.admonition), .coal :is(.admonition) { + background-color: var(--theme-hover); +} + +.rust :is(.admonition) { + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); +} +.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited { + color: var(--sidebar-fg); +} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f64b0440..d7cead82 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,21 +1,10 @@ # Summary -[FROST](README.md) +[ZF FROST](index.md) +- [Understanding FROST](frost.md) +- [Tutorial](tutorial.md) + - [Distributed Key Generation](tutorial/dkg.md) - [User Documentation](user.md) - - [frost-core](user/frost-core.md) - - [frost-rerandomized](user/frost-rerandomized.md) - - [frost-ed25519](user/frost-ed25519.md) - - [DKG](user/frost-ed25519/dkg.md) - - [frost-ed448](user/frost-ed448.md) - - [DKG](user/frost-ed448/dkg.md) - - [frost-p256](user/frost-p256.md) - - [DKG](user/frost-p256/dkg.md) - - [frost-ristretto255](user/frost-ristretto255.md) - - [DKG](user/frost-ristretto255/dkg.md) - - [frost-secp256k1](user/frost-secp256k1.md) - - [DKG](user/frost-secp256k1/dkg.md) - [Terminology](terminology.md) - [Developer Documentation](dev.md) - - [FROST RFCs](dev/rfcs.md) - - [FROST messages](dev/rfcs/0001-messages.md ) - [List of Dependencies for Audit](dev/frost-dependencies-for-audit.md) diff --git a/book/src/dev.md b/book/src/dev.md index 4796167e..3edcfeb9 100644 --- a/book/src/dev.md +++ b/book/src/dev.md @@ -1 +1,4 @@ # Developer Documentation + +This section contains information only relevant to ZF FROST developers or +contributors. diff --git a/book/src/dev/rfcs.md b/book/src/dev/rfcs.md deleted file mode 100644 index d811484c..00000000 --- a/book/src/dev/rfcs.md +++ /dev/null @@ -1 +0,0 @@ -# FROST RFCs diff --git a/book/src/dev/rfcs/0001-messages.md b/book/src/dev/rfcs/0001-messages.md deleted file mode 100644 index e5748f1a..00000000 --- a/book/src/dev/rfcs/0001-messages.md +++ /dev/null @@ -1 +0,0 @@ -{{#include ../../../../rfcs/0001-messages.md}} \ No newline at end of file diff --git a/book/src/frost.md b/book/src/frost.md new file mode 100644 index 00000000..86632f1a --- /dev/null +++ b/book/src/frost.md @@ -0,0 +1,92 @@ +# Understanding FROST + +This explains the main concepts and flows of FROST in a generic manner. These +are important to understand how to use the library, but rest assured that the +[Tutorial](tutorial.md) will have more concrete information. + +FROST is a threshold signature scheme. It allows splitting a Schnorr signing key +into `n` shares for a threshold `t`, such that `t` (or more) participants can +together generate a signature that can be validated by the corresponding verifying +key. One important aspect is that the resulting signature is indistinguishable from a +non-threshold signature from the point of view of signature verifiers. + +```admonish note +FROST only supports Schnorr signatures. Therefore it can't produce +ECDSA signatures. +``` + +## Key Generation + +There are two options for generating FROST key shares. In both cases, after the +key generation procedure, each participant will get: + +- a **secret share**; +- a **verifying share** (which can be used by other participants to verify the + signature shares the participant produces); +- a **group verifying key**, which is the public key matching the private key that was + split into shares; it is used to verify the final signature generated with FROST. + +### Trusted Dealer Generation + +An existing key (which can be freshly generated) is split into shares. It's the +simplest approach, but it has the downside of requiring the entire key to exist +in memory at some point in time, which may not be desired in high security +applications. However, it is much simpler to set up. It requires an +[authenticated and confidential communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) to +distribute each share to their respective participants. + +[Learn how to do Trusted Dealer Generation with the ZF FROST library](tutorial.md#generating-key-shares-with-a-trusted-dealer). + +### Distribtuted Key Generation + +A two-round protocol after which each participant will have their share of the +secret, without the secret being ever present in its entirety in any +participant's memory. Its downside is that it requires a [broadcast +channel](https://frost.zfnd.org/terminology.html#broadcast-channel) as well as +an [authenticated and confidential communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) between +each pair of participants, which may be difficult to deploy in practice. + +[Learn how to do Distributed Key Generation with the ZF FROST +library](tutorial/dkg.md). + +## Signing + +Signing with FROST starts with a Coordinator (which can be one of the +share holders, or not) which selects the message to be signed and +the participants that will generate the signature. + +Each participant sends fresh nonce commitments to the Coordinator, which then +consolidates them and sends them to each participant. Each one will then produce +a signature share, which is sent to the Coordinator who finally aggregates them +and produces the final signature. + +```admonish note +If having a single coordinator is not desired, then all participants +can act as coordinators. Refer to the +[spec](https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#removing-the-coordinator-role-no-coordinator) +for more information. +``` + +```admonish warning +ALL participants who are selected for generating the signature need +to produce their share, even if there are more than `t` of them. +For example, in 2-of-3 signing, if 3 participants are selected, +them all 3 must produce signature shares in order for the Coordinator +be able to produce the final signature. Of course, the Coordinator +is still free to start the process with only 2 participants if they wish. +``` + +## Verifying + +Signature verification is carried out as normal with single-party signatures, +along with the signed message and the group verifying key as inputs. + + +## Ciphersuites + +FROST is a generic protocol that works with any adequate prime-order group, +which in practice are constructed from elliptic curves. The spec specifies +five ciphersuites with the Ristretto255, Ed25519, Ed448, P-256 and secp256k1 +groups. It's possible (though not recommended) to use your own ciphersuite. diff --git a/book/src/index.md b/book/src/index.md new file mode 100644 index 00000000..66fad0a2 --- /dev/null +++ b/book/src/index.md @@ -0,0 +1,11 @@ +# The ZF FROST Book + +This is a guide-level reference for the [ZF FROST library](https://github.com/ZcashFoundation/frost/). + +## Getting Started + +If you're not familiar with FROST, first read [Understanding FROST](frost.md). + +Then read the [Tutorial](tutorial.md), and use the [Rust +docs](user.md) as +reference. \ No newline at end of file diff --git a/book/src/terminology.md b/book/src/terminology.md index d8517a0c..f0265d5a 100644 --- a/book/src/terminology.md +++ b/book/src/terminology.md @@ -16,6 +16,16 @@ Possible deployment options: - Posting commitments to an authenticated centralized server that is trusted to provide a single view to all participants (also known as 'public bulletin board') +### _Identifier_ + +An identifier is a non-zero scalar (i.e. a number in a range specific to the +ciphersuite) which identifies a specific party. There are no restrictions to +them other than being unique for each participant and being in the valid range. + +In the ZF FROST library, they are either automatically generated incrementally +during key generation or can be instantiated from a byte array using +[`Identifier::deserialize()`](https://docs.rs/frost-core/latest/frost_core/frost/struct.Identifier.html#method.deserialize). + ### _Peer to peer channel_ Peer-to-peer channels are authenticated, reliable, and unordered, per the diff --git a/book/src/tutorial.md b/book/src/tutorial.md new file mode 100644 index 00000000..e06d033c --- /dev/null +++ b/book/src/tutorial.md @@ -0,0 +1,242 @@ +# Tutorial + +The ZF FROST suite consists of multiple crates. `frost-core` contains +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 +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 +`frost-core` along with the ciphersuite types. + +This tutorial will use the `frost-ristretto255` crate, but changing +to another ciphersuite should be a matter of simply changing the import. + +## Including `frost-ristretto255` + +Add to your `Cargo.toml` file: + +``` +[dependencies] +frost-ristretto255 = "0.3.0" +``` + +## Handling errors + +Most crate functions mentioned below return `Result`s with +[`Error`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/type.Error.html)s. +All errors should be considered fatal and should lead to aborting the key +generation or signing procedure. + +## Serializing structures + +FROST is a distributed protocol and thus it requires sending messages between +participants. However, the ZF FROST library does not handle communication nor +encoding, which is the application's responsibility. For this reason, all +structures that need to be transmitted have public fields allowing the +application to encode and decode them as it wishes. (Note that fields like +`Scalar` and `Element` do have standard encodings; only the serialization of the +structure itself and things like maps and lists need to be handled by the +application.) + +The ZF FROST library will also support `serde` in the future, which will make +this process simpler. + + +## Generating key shares with a trusted dealer + +The diagram below shows the trusted dealer key generation process. Dashed lines +represent data being sent through an [authenticated and confidential communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + +![Diagram of Trusted Dealer Key Generation, illustrating what is explained in the text](tutorial/tkg.png) + +To generate the key shares, the dealer calls +[`generate_with_dealer()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/fn.generate_with_dealer.html). +It returns a `HashMap` mapping the (automatically generated) `Identifier`s to +their respective `SecretShare`s, and a `PublicKeyPackage` which contains the +`VerifyingShare` for each participant and the group public key (`VerifyingKey`). + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:tkg_gen}} +``` + +Each `SecretShare` must then be sent via an [**authenticated** and +**confidential** channel +](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) for each +participant, who must verify the package to obtain a `KeyPackage` which contains +their signing share, verifying share and group verifying key. This is done with +[`KeyPackage::try_from()`](https://docs.rs/frost-core/latest/frost_core/frost/keys/struct.KeyPackage.html#method.try_from): + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:tkg_verify}} +``` + +```admonish info +Currently there is no way to specify which identifiers to use. This will +likely be supported in the future. + +[More information on how to handle Identifiers](https://frost.zfnd.org/terminology.html#identifier). +``` + +```admonish danger +Which [**authenticated** and **confidential** channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) +to use is up to the application. Some examples: + +- Manually require the dealer to sent the `SecretShare`s to the + partipants using some secure messenger such as Signal; +- Use a TLS connection, authenticating the server with a certificate + and the client with some user/password or another suitable authentication + mechanism; + +Refer to the [Terminology page](https://frost.zfnd.org/terminology.html#peer-to-peer-channel) +for more details. + +Failure of using a **confidential** channel may lead to the shares being +stolen and possibly allowing signature forgeries if a threshold number of +them are stolen. + +Failure of using an **authenticated** channel may lead to shares being +sent to the wrong person, possibly allowing unintended parties +to generate signatures. +``` + +```admonish danger +The `SecretPackage` contents must be stored securely. For example: + +- Make sure other users in the system can't read it; +- If possible, use the OS secure storage such that the package + contents can only be opened with the user's password or biometrics. +``` + +```admonish warning +The participants may wish to not fully trust the dealer. While **the dealer +has access to the original secret and can forge signatures +by simply using the secret to sign** (and this can't be +possibly avoided with this method; use Distributed Key Generation +if that's an issue), the dealer could also tamper with the `SecretShare`s +in a way that the participants will never be able to generate a valid +signature in the future (denial of service). Participants can detect +such tampering by comparing the `VerifiableSecretSharingCommitment` +values from their `SecretShare`s (either by some manual process, or +by using a [broadcast channel](https://frost.zfnd.org/terminology.html#broadcast-channel)) +to make sure they are all equal. +``` + +## Signing + +The diagram below shows the signing process. Dashed lines represent data being +sent through an [authenticated communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + +![Diagram of Signing, illustrating what is explained in the text](tutorial/signing.png) + +### Coordinator, Round 1 + +To sign, the +[Coordinator](file:///home/conrado/zfnd/frost/book/book/frost.html#signing) must +select which participants are going to generate the signature, and must signal +to start the process. This needs to be implemented by users of the ZF FROST library and will depend on +the communication channel being used. + +### Participants, Round 1 + +Each selected participant will then generate the nonces (a `SigningNonces`) and +their commitments (a `SigningCommitments`) by calling +[`round1::commit()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/round1/fn.commit.html): + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:round1_commit}} +``` + +The `SigningNonces` must be kept by the participant to use in Round 2, while the +`SigningCommitments` must be sent to the Coordinator using an [authenticated +channel](https://frost.zfnd.org/terminology.html#broadcast-channel). + +### Coordinator, Round 2 + +The Coordinator will get all `SigningCommitments` from the participants and the +message to be signed, and then build a `SigningPackage` by calling +[`SigningPackage::new()`](https://docs.rs/frost-core/latest/frost_core/frost/struct.SigningPackage.html#method.new). + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:round2_package}} +``` + +The `SigningPackage` must then be sent to all the participants using an +[authenticated +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). (Of course, +if the message is confidential, then the channel must also be confidential.) + +```admonish warning +In all of the main FROST ciphersuites, the entire message must +be sent to participants. In some cases, where the message is too big, it may be +necessary to send a hash of the message instead. We strongly suggest creating a +specific ciphersuite for this, and not just sending the hash as if it were the +message. For reference, see [how RFC 8032 handles +"pre-hashing"](https://datatracker.ietf.org/doc/html/rfc8032). +``` + +### Participants, Round 2 + +Upon receiving the `SigningPackage`, each participant will then produce their +signature share using their `KeyPackage` from the key generation process and +their `SigningNonces` from Round 1, by calling +[`round2::sign()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/round2/fn.sign.html): + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:round2_sign}} +``` + +The resulting `SignatureShare` must then be sent back to the Coordinator using +an [authenticated +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + +```admonish important +In most applications, it is important that the participant must be aware of what +they are signing. Thus the application should show the message to the +participant and obtain their consent to proceed before producing the signature +share. +``` + +### Coordinator, Aggregate + +Upon receiving the `SignatureShare`s from the participants, the Coordinator can +finally produce the final signature by calling +[`aggregate()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/fn.aggregate.html) +with the same `SigningPackage` sent to the participants and the +`PublicKeyPackage` from the key generation (which is used to validate each +`SignatureShare`). + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:aggregate}} +``` + +The returned signature, a `Signature`, will be a valid signature for the message +chosen in Round 2 for the group verifying key in the `PublicKeyPackage`. + +```admonish note +FROST supports identifiable abort: if a participant misbehaves and produces an +invalid signature share, then aggregation will fail and the returned error +will have the identifier of the misbehaving participant. (If multiple participants +misbehave, only the first one detected will be returned.) + +What should be done in that case is up to the application. The misbehaving participant +could be excluded from future signing sessions, for example. +``` + + +## Verifying signatures + +The Coordinator could verify the signature with: + +```rust,no_run,noplayground +{{#include ../../frost-ristretto255/README.md:verify}} +``` + +(There is no need for the Coordinator to verify the signature since that already +happens inside `aggregate()`. This just shows how the signature can be +verified.) diff --git a/book/src/tutorial/dkg.md b/book/src/tutorial/dkg.md new file mode 100644 index 00000000..3e8a8540 --- /dev/null +++ b/book/src/tutorial/dkg.md @@ -0,0 +1,82 @@ +# Distributed Key Generation + +The diagram below shows the distributed key generation process. Dashed lines +represent data being sent through an [authenticated and confidential +communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). Note +that the first dashed line requires a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) + +![Diagram of Distribtuted Key Generation, illustrating what is explained in the text](dkg/dkg.png) + +## Part 1 + +To start the DKG, each participant calls +[`dkg::part1()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/fn.part1.html) +passing its identifier, the desired threshold and total number of participants. +(Thus, they need to agree on those parameters via some mechanism which is up to +the application.) It returns a `round1::SecretPackage` and a `round1::Package`: + +```rust,no_run,noplayground +{{#include ../../../frost-ristretto255/dkg.md:dkg_import}} + +// create `participant_identifier` somehow + +{{#include ../../../frost-ristretto255/dkg.md:dkg_part1}} +``` + +The `round1::SecretPackage` must be kept in memory to use in the next round. The +`round1::Package` must be sent to all other participants using a [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) to ensure +that all participants receive the same value. + +```admonish danger +A [**broadcast +channel**](https://frost.zfnd.org/terminology.html#broadcast-channel) in this +context is not simply broadcasting the value to all participants. It requires +running a protocol to ensure that all participants have the same value or that +the protocol is aborted. Check the linked [Terminology +section](https://frost.zfnd.org/terminology.html#broadcast-channel) for more +details. + +**Failure in using a proper broadcast channel will make the key generation +insecure.** +``` + +## Part 2 + +Upon receiving the other participants' `round1::Package`s, each participant then +calls +[`dkg::part2()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/fn.part2.html) +passing their own previously created `round1::SecretPackage` and the list of +received `round1::Packages`. It returns a `round2::SecretPackage` and a +`HashMap` mapping other participants's `Identifier`s to `round2::Package`s: + +```rust,no_run,noplayground +{{#include ../../../frost-ristretto255/dkg.md:dkg_part2}} +``` + +The `round2::SecretPackage` must be kept in memory for the next part; the +`round1::SecretPackage` is consumed and is not required anymore. + +The `round2::Package`s must be sent to their respective participants with the +given `Identifier`s, using an [authenticated and confidential communication +channel](https://frost.zfnd.org/terminology.html#peer-to-peer-channel). + +## Part 3 + +Finally, upon receiving the other participant's `round2::Package`, the DKG is +concluded by calling +[`dkg::part3()`](https://docs.rs/frost-ristretto255/latest/frost_ristretto255/keys/dkg/fn.part3.html) +passing the same `round1::Package`s received in Part 2, the `round2::Package`s +just received, and the previously stored `round2::SecretPackage` for the +participant. It returns a `KeyPackage`, with the participant's secret share, +and a `PublicKeyPackage` containing the group verifying key: + +```rust,no_run,noplayground +{{#include ../../../frost-ristretto255/dkg.md:dkg_part3}} +``` + +```admonish note +All participants will generate the same `PublicKeyPackage`. +``` \ No newline at end of file diff --git a/book/src/tutorial/dkg/dkg.png b/book/src/tutorial/dkg/dkg.png new file mode 100644 index 00000000..4fa2c049 Binary files /dev/null and b/book/src/tutorial/dkg/dkg.png differ diff --git a/book/src/tutorial/signing.png b/book/src/tutorial/signing.png new file mode 100644 index 00000000..5d9383ea Binary files /dev/null and b/book/src/tutorial/signing.png differ diff --git a/book/src/tutorial/tkg.png b/book/src/tutorial/tkg.png new file mode 100644 index 00000000..4b9fa2bd Binary files /dev/null and b/book/src/tutorial/tkg.png differ diff --git a/book/src/user.md b/book/src/user.md index 3a51d9a2..30c1cc8b 100644 --- a/book/src/user.md +++ b/book/src/user.md @@ -1 +1,9 @@ # User Documentation + +- [frost-core](https://docs.rs/frost-core/) +- [frost-rerandomized](https://docs.rs/frost-rerandomized/) +- [frost-ed25519](https://docs.rs/frost-ristretto255/) +- [frost-ed448](https://docs.rs/frost-ed448/) +- [frost-p256](https://docs.rs/frost-p256/) +- [frost-ristretto255](https://docs.rs/frost-ristretto255/) +- [frost-secp256k1](https://docs.rs/frost-secp256k1/) diff --git a/frost-core/src/tests/ciphersuite_generic.rs b/frost-core/src/tests/ciphersuite_generic.rs index 114d4d9f..ba1f9ac7 100644 --- a/frost-core/src/tests/ciphersuite_generic.rs +++ b/frost-core/src/tests/ciphersuite_generic.rs @@ -81,10 +81,11 @@ fn check_sign( min_signers: u16, key_packages: HashMap, frost::keys::KeyPackage>, mut rng: R, - pubkeys: frost::keys::PublicKeyPackage, + pubkey_package: frost::keys::PublicKeyPackage, ) -> (Vec, Signature, VerifyingKey) { - let mut nonces: HashMap, frost::round1::SigningNonces> = HashMap::new(); - let mut commitments: HashMap, frost::round1::SigningCommitments> = + let mut nonces_map: HashMap, frost::round1::SigningNonces> = + HashMap::new(); + let mut commitments_map: HashMap, frost::round1::SigningCommitments> = HashMap::new(); //////////////////////////////////////////////////////////////////////////// @@ -95,7 +96,7 @@ fn check_sign( let participant_identifier = participant_index.try_into().expect("should be nonzero"); // Generate one (1) nonce and one SigningCommitments instance for each // participant, up to _min_signers_. - let (nonce, commitment) = frost::round1::commit( + let (nonces, commitments) = frost::round1::commit( participant_identifier, key_packages .get(&participant_identifier) @@ -103,8 +104,8 @@ fn check_sign( .secret_share(), &mut rng, ); - nonces.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); } // This is what the signature aggregator / coordinator needs to do: @@ -112,17 +113,17 @@ fn check_sign( // - take one (unused) commitment per signing participant let mut signature_shares = Vec::new(); let message = "message to sign".as_bytes(); - let comms = commitments.clone().into_values().collect(); + let comms = commitments_map.clone().into_values().collect(); let signing_package = frost::SigningPackage::new(comms, message.to_vec()); //////////////////////////////////////////////////////////////////////////// // Round 2: each participant generates their signature share //////////////////////////////////////////////////////////////////////////// - for participant_identifier in nonces.keys() { + for participant_identifier in nonces_map.keys() { let key_package = key_packages.get(participant_identifier).unwrap(); - let nonces_to_use = &nonces.get(participant_identifier).unwrap(); + let nonces_to_use = &nonces_map.get(participant_identifier).unwrap(); // Each participant generates their signature share. let signature_share = @@ -136,22 +137,20 @@ fn check_sign( //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) - let group_signature_res = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys); - - assert!(group_signature_res.is_ok()); - - let group_signature = group_signature_res.unwrap(); + let group_signature = + frost::aggregate(&signing_package, &signature_shares[..], &pubkey_package).unwrap(); // Check that the threshold signature can be verified by the group public // key (the verification key). - assert!(pubkeys + let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); + assert!(is_signature_valid); // Check that the threshold signature can be verified by the group public // key (the verification key) from KeyPackage.group_public - for (participant_identifier, _) in nonces.clone() { + for (participant_identifier, _) in nonces_map.clone() { let key_package = key_packages.get(&participant_identifier).unwrap(); assert!(key_package @@ -160,7 +159,11 @@ fn check_sign( .is_ok()); } - (message.to_owned(), group_signature, pubkeys.group_public) + ( + message.to_owned(), + group_signature, + pubkey_package.group_public, + ) } /// Test FROST signing with trusted dealer with a Ciphersuite. @@ -197,13 +200,13 @@ where // 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 (secret_package, round1_package) = + let (round1_secret_package, round1_package) = frost::keys::dkg::part1(participant_identifier, max_signers, min_signers, &mut rng) .unwrap(); // Store the participant's secret package for later use. // In practice each participant will store it in their own environment. - round1_secret_packages.insert(participant_identifier, secret_package); + 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 HashMap; in practice this will be @@ -240,15 +243,12 @@ where // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - received_round1_packages - .get(&participant_identifier) - .unwrap(), - ) - .expect("should work"); + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + let (round2_secret_package, round2_packages) = + frost::keys::dkg::part2(round1_secret_package, round1_packages).expect("should work"); // Store the participant's secret package for later use. // In practice each participant will store it in their own environment. diff --git a/frost-ed25519/README.md b/frost-ed25519/README.md index 870b8510..8ced05e4 100644 --- a/frost-ed25519/README.md +++ b/frost-ed25519/README.md @@ -9,6 +9,7 @@ scenario in a single thread and it abstracts away any communication between peer ```rust +# // ANCHOR: tkg_gen use frost_ed25519 as frost; use rand::thread_rng; use std::collections::HashMap; @@ -16,20 +17,23 @@ use std::collections::HashMap; let mut rng = thread_rng(); let max_signers = 5; let min_signers = 3; -let (shares, pubkeys) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +let (shares, pubkey_package) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +# // ANCHOR_END: tkg_gen // Verifies the secret shares from the dealer and store them in a HashMap. // In practice, the KeyPackages must be sent to its respective participants // through a confidential and authenticated channel. let mut key_packages: HashMap<_, _> = HashMap::new(); -for (k, v) in shares { - let key_package = frost::keys::KeyPackage::try_from(v)?; - key_packages.insert(k, key_package); +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 = HashMap::new(); -let mut commitments = HashMap::new(); +let mut nonces_map = HashMap::new(); +let mut commitments_map = HashMap::new(); //////////////////////////////////////////////////////////////////////////// // Round 1: generating nonces and signing commitments for each participant @@ -38,43 +42,51 @@ let mut commitments = HashMap::new(); // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..(min_signers as u16 + 1) { 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_. - let (nonce, commitment) = frost::round1::commit( + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( participant_identifier, - key_packages[&participant_identifier].secret_share(), + key_package.secret_share(), &mut rng, ); - // In practice, the nonces and commitment must be sent to the coordinator + # // 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.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + 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 = Vec::new(); +let commitments_received = commitments_map.clone().into_values().collect(); +# // ANCHOR: round2_package let message = "message to sign".as_bytes(); -let comms = commitments.clone().into_values().collect(); -// 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(comms, message.to_vec()); +# // 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_received, message.to_vec()); +# // 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.keys() { +for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; - let nonces_to_use = &nonces[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + # // 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. @@ -87,14 +99,20 @@ for participant_identifier in nonces.keys() { //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) -let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; +# // 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). -assert!(pubkeys +# // ANCHOR: verify +let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); # Ok::<(), frost::Error>(()) ``` diff --git a/frost-ed25519/dkg.md b/frost-ed25519/dkg.md index 3bb952a7..a0cf95d9 100644 --- a/frost-ed25519/dkg.md +++ b/frost-ed25519/dkg.md @@ -25,6 +25,7 @@ they can proceed to sign messages with FROST. ## Example ```rust +# // ANCHOR: dkg_import use rand::thread_rng; use std::collections::HashMap; @@ -32,13 +33,14 @@ use frost_ed25519 as frost; let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 //////////////////////////////////////////////////////////////////////////// -let max_signers = 5; -let min_signers = 3; - // 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. @@ -53,16 +55,18 @@ let mut received_round1_packages = HashMap::new(); // 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 (secret_package, round1_package) = frost::keys::dkg::part1( + # // 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, secret_package); + 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 HashMap; in practice this will be @@ -99,12 +103,14 @@ let mut received_round2_packages = HashMap::new(); // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - &received_round1_packages[&participant_identifier], - )?; + 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. @@ -142,13 +148,18 @@ let mut pubkey_packages = HashMap::new(); // 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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::part3( - &round2_secret_packages[&participant_identifier], - &received_round1_packages[&participant_identifier], - &received_round2_packages[&participant_identifier], + 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_for_participant); + pubkey_packages.insert(participant_identifier, pubkey_package); } // With its own key package and the pubkey package, each participant can now proceed diff --git a/frost-ed448/README.md b/frost-ed448/README.md index d5463c21..fde4bfe9 100644 --- a/frost-ed448/README.md +++ b/frost-ed448/README.md @@ -9,6 +9,7 @@ scenario in a single thread and it abstracts away any communication between peer ```rust +# // ANCHOR: tkg_gen use frost_ed448 as frost; use rand::thread_rng; use std::collections::HashMap; @@ -16,20 +17,23 @@ use std::collections::HashMap; let mut rng = thread_rng(); let max_signers = 5; let min_signers = 3; -let (shares, pubkeys) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +let (shares, pubkey_package) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +# // ANCHOR_END: tkg_gen // Verifies the secret shares from the dealer and store them in a HashMap. // In practice, the KeyPackages must be sent to its respective participants // through a confidential and authenticated channel. let mut key_packages: HashMap<_, _> = HashMap::new(); -for (k, v) in shares { - let key_package = frost::keys::KeyPackage::try_from(v)?; - key_packages.insert(k, key_package); +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 = HashMap::new(); -let mut commitments = HashMap::new(); +let mut nonces_map = HashMap::new(); +let mut commitments_map = HashMap::new(); //////////////////////////////////////////////////////////////////////////// // Round 1: generating nonces and signing commitments for each participant @@ -38,43 +42,51 @@ let mut commitments = HashMap::new(); // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..(min_signers as u16 + 1) { 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_. - let (nonce, commitment) = frost::round1::commit( + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( participant_identifier, - key_packages[&participant_identifier].secret_share(), + key_package.secret_share(), &mut rng, ); - // In practice, the nonces and commitment must be sent to the coordinator + # // 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.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + 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 = Vec::new(); +let commitments_received = commitments_map.clone().into_values().collect(); +# // ANCHOR: round2_package let message = "message to sign".as_bytes(); -let comms = commitments.clone().into_values().collect(); -// 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(comms, message.to_vec()); +# // 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_received, message.to_vec()); +# // 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.keys() { +for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; - let nonces_to_use = &nonces[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + # // 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. @@ -87,14 +99,20 @@ for participant_identifier in nonces.keys() { //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) -let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; +# // 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). -assert!(pubkeys +# // ANCHOR: verify +let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); # Ok::<(), frost::Error>(()) ``` diff --git a/frost-ed448/dkg.md b/frost-ed448/dkg.md index 3ee4f3f9..54212cfe 100644 --- a/frost-ed448/dkg.md +++ b/frost-ed448/dkg.md @@ -25,6 +25,7 @@ they can proceed to sign messages with FROST. ## Example ```rust +# // ANCHOR: dkg_import use rand::thread_rng; use std::collections::HashMap; @@ -32,13 +33,14 @@ use frost_ed448 as frost; let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 //////////////////////////////////////////////////////////////////////////// -let max_signers = 5; -let min_signers = 3; - // 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. @@ -53,16 +55,18 @@ let mut received_round1_packages = HashMap::new(); // 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 (secret_package, round1_package) = frost::keys::dkg::part1( + # // 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, secret_package); + 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 HashMap; in practice this will be @@ -99,12 +103,14 @@ let mut received_round2_packages = HashMap::new(); // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - &received_round1_packages[&participant_identifier], - )?; + 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. @@ -142,13 +148,18 @@ let mut pubkey_packages = HashMap::new(); // 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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::part3( - &round2_secret_packages[&participant_identifier], - &received_round1_packages[&participant_identifier], - &received_round2_packages[&participant_identifier], + 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_for_participant); + pubkey_packages.insert(participant_identifier, pubkey_package); } // With its own key package and the pubkey package, each participant can now proceed diff --git a/frost-p256/README.md b/frost-p256/README.md index 81548c83..b33e4b84 100644 --- a/frost-p256/README.md +++ b/frost-p256/README.md @@ -9,6 +9,7 @@ scenario in a single thread and it abstracts away any communication between peer ```rust +# // ANCHOR: tkg_gen use frost_p256 as frost; use rand::thread_rng; use std::collections::HashMap; @@ -16,20 +17,23 @@ use std::collections::HashMap; let mut rng = thread_rng(); let max_signers = 5; let min_signers = 3; -let (shares, pubkeys) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +let (shares, pubkey_package) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +# // ANCHOR_END: tkg_gen // Verifies the secret shares from the dealer and store them in a HashMap. // In practice, the KeyPackages must be sent to its respective participants // through a confidential and authenticated channel. let mut key_packages: HashMap<_, _> = HashMap::new(); -for (k, v) in shares { - let key_package = frost::keys::KeyPackage::try_from(v)?; - key_packages.insert(k, key_package); +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 = HashMap::new(); -let mut commitments = HashMap::new(); +let mut nonces_map = HashMap::new(); +let mut commitments_map = HashMap::new(); //////////////////////////////////////////////////////////////////////////// // Round 1: generating nonces and signing commitments for each participant @@ -38,43 +42,51 @@ let mut commitments = HashMap::new(); // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..(min_signers as u16 + 1) { 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_. - let (nonce, commitment) = frost::round1::commit( + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( participant_identifier, - key_packages[&participant_identifier].secret_share(), + key_package.secret_share(), &mut rng, ); - // In practice, the nonces and commitment must be sent to the coordinator + # // 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.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + 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 = Vec::new(); +let commitments_received = commitments_map.clone().into_values().collect(); +# // ANCHOR: round2_package let message = "message to sign".as_bytes(); -let comms = commitments.clone().into_values().collect(); -// 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(comms, message.to_vec()); +# // 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_received, message.to_vec()); +# // 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.keys() { +for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; - let nonces_to_use = &nonces[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + # // 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. @@ -87,14 +99,20 @@ for participant_identifier in nonces.keys() { //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) -let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; +# // 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). -assert!(pubkeys +# // ANCHOR: verify +let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); # Ok::<(), frost::Error>(()) ``` diff --git a/frost-p256/dkg.md b/frost-p256/dkg.md index ab515ed3..ff3b80f4 100644 --- a/frost-p256/dkg.md +++ b/frost-p256/dkg.md @@ -25,6 +25,7 @@ they can proceed to sign messages with FROST. ## Example ```rust +# // ANCHOR: dkg_import use rand::thread_rng; use std::collections::HashMap; @@ -32,13 +33,14 @@ use frost_p256 as frost; let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 //////////////////////////////////////////////////////////////////////////// -let max_signers = 5; -let min_signers = 3; - // 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. @@ -53,16 +55,18 @@ let mut received_round1_packages = HashMap::new(); // 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 (secret_package, round1_package) = frost::keys::dkg::part1( + # // 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, secret_package); + 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 HashMap; in practice this will be @@ -99,12 +103,14 @@ let mut received_round2_packages = HashMap::new(); // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - &received_round1_packages[&participant_identifier], - )?; + 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. @@ -142,13 +148,18 @@ let mut pubkey_packages = HashMap::new(); // 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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::part3( - &round2_secret_packages[&participant_identifier], - &received_round1_packages[&participant_identifier], - &received_round2_packages[&participant_identifier], + 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_for_participant); + pubkey_packages.insert(participant_identifier, pubkey_package); } // With its own key package and the pubkey package, each participant can now proceed diff --git a/frost-ristretto255/README.md b/frost-ristretto255/README.md index a3ac17b9..c7d0625b 100644 --- a/frost-ristretto255/README.md +++ b/frost-ristretto255/README.md @@ -9,6 +9,7 @@ scenario in a single thread and it abstracts away any communication between peer ```rust +# // ANCHOR: tkg_gen use frost_ristretto255 as frost; use rand::thread_rng; use std::collections::HashMap; @@ -16,20 +17,23 @@ use std::collections::HashMap; let mut rng = thread_rng(); let max_signers = 5; let min_signers = 3; -let (shares, pubkeys) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +let (shares, pubkey_package) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +# // ANCHOR_END: tkg_gen // Verifies the secret shares from the dealer and store them in a HashMap. // In practice, the KeyPackages must be sent to its respective participants // through a confidential and authenticated channel. let mut key_packages: HashMap<_, _> = HashMap::new(); -for (k, v) in shares { - let key_package = frost::keys::KeyPackage::try_from(v)?; - key_packages.insert(k, key_package); +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 = HashMap::new(); -let mut commitments = HashMap::new(); +let mut nonces_map = HashMap::new(); +let mut commitments_map = HashMap::new(); //////////////////////////////////////////////////////////////////////////// // Round 1: generating nonces and signing commitments for each participant @@ -38,43 +42,51 @@ let mut commitments = HashMap::new(); // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..(min_signers as u16 + 1) { 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_. - let (nonce, commitment) = frost::round1::commit( + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( participant_identifier, - key_packages[&participant_identifier].secret_share(), + key_package.secret_share(), &mut rng, ); - // In practice, the nonces and commitment must be sent to the coordinator + # // 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.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + 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 = Vec::new(); +let commitments_received = commitments_map.clone().into_values().collect(); +# // ANCHOR: round2_package let message = "message to sign".as_bytes(); -let comms = commitments.clone().into_values().collect(); -// 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(comms, message.to_vec()); +# // 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_received, message.to_vec()); +# // 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.keys() { +for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; - let nonces_to_use = &nonces[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + # // 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. @@ -87,14 +99,20 @@ for participant_identifier in nonces.keys() { //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) -let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; +# // 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). -assert!(pubkeys +# // ANCHOR: verify +let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); # Ok::<(), frost::Error>(()) ``` diff --git a/frost-ristretto255/dkg.md b/frost-ristretto255/dkg.md index c7de1c29..3aee3284 100644 --- a/frost-ristretto255/dkg.md +++ b/frost-ristretto255/dkg.md @@ -25,6 +25,7 @@ they can proceed to sign messages with FROST. ## Example ```rust +# // ANCHOR: dkg_import use rand::thread_rng; use std::collections::HashMap; @@ -32,13 +33,14 @@ use frost_ristretto255 as frost; let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 //////////////////////////////////////////////////////////////////////////// -let max_signers = 5; -let min_signers = 3; - // 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. @@ -53,16 +55,18 @@ let mut received_round1_packages = HashMap::new(); // 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 (secret_package, round1_package) = frost::keys::dkg::part1( + # // 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, secret_package); + 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 HashMap; in practice this will be @@ -99,12 +103,14 @@ let mut received_round2_packages = HashMap::new(); // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - &received_round1_packages[&participant_identifier], - )?; + 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. @@ -142,13 +148,18 @@ let mut pubkey_packages = HashMap::new(); // 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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::part3( - &round2_secret_packages[&participant_identifier], - &received_round1_packages[&participant_identifier], - &received_round2_packages[&participant_identifier], + 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_for_participant); + pubkey_packages.insert(participant_identifier, pubkey_package); } // With its own key package and the pubkey package, each participant can now proceed diff --git a/frost-secp256k1/README.md b/frost-secp256k1/README.md index c27042a9..ebc79da5 100644 --- a/frost-secp256k1/README.md +++ b/frost-secp256k1/README.md @@ -9,6 +9,7 @@ scenario in a single thread and it abstracts away any communication between peer ```rust +# // ANCHOR: tkg_gen use frost_secp256k1 as frost; use rand::thread_rng; use std::collections::HashMap; @@ -16,20 +17,23 @@ use std::collections::HashMap; let mut rng = thread_rng(); let max_signers = 5; let min_signers = 3; -let (shares, pubkeys) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +let (shares, pubkey_package) = frost::keys::generate_with_dealer(max_signers, min_signers, &mut rng)?; +# // ANCHOR_END: tkg_gen // Verifies the secret shares from the dealer and store them in a HashMap. // In practice, the KeyPackages must be sent to its respective participants // through a confidential and authenticated channel. let mut key_packages: HashMap<_, _> = HashMap::new(); -for (k, v) in shares { - let key_package = frost::keys::KeyPackage::try_from(v)?; - key_packages.insert(k, key_package); +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 = HashMap::new(); -let mut commitments = HashMap::new(); +let mut nonces_map = HashMap::new(); +let mut commitments_map = HashMap::new(); //////////////////////////////////////////////////////////////////////////// // Round 1: generating nonces and signing commitments for each participant @@ -38,43 +42,51 @@ let mut commitments = HashMap::new(); // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..(min_signers as u16 + 1) { 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_. - let (nonce, commitment) = frost::round1::commit( + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( participant_identifier, - key_packages[&participant_identifier].secret_share(), + key_package.secret_share(), &mut rng, ); - // In practice, the nonces and commitment must be sent to the coordinator + # // 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.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); + 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 = Vec::new(); +let commitments_received = commitments_map.clone().into_values().collect(); +# // ANCHOR: round2_package let message = "message to sign".as_bytes(); -let comms = commitments.clone().into_values().collect(); -// 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(comms, message.to_vec()); +# // 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_received, message.to_vec()); +# // 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.keys() { +for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; - let nonces_to_use = &nonces[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + # // 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. @@ -87,14 +99,20 @@ for participant_identifier in nonces.keys() { //////////////////////////////////////////////////////////////////////////// // Aggregate (also verifies the signature shares) -let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; +# // 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). -assert!(pubkeys +# // ANCHOR: verify +let is_signature_valid = pubkey_package .group_public .verify(message, &group_signature) - .is_ok()); + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); # Ok::<(), frost::Error>(()) ``` diff --git a/frost-secp256k1/dkg.md b/frost-secp256k1/dkg.md index c9c108e6..8efe0539 100644 --- a/frost-secp256k1/dkg.md +++ b/frost-secp256k1/dkg.md @@ -25,6 +25,7 @@ they can proceed to sign messages with FROST. ## Example ```rust +# // ANCHOR: dkg_import use rand::thread_rng; use std::collections::HashMap; @@ -32,13 +33,14 @@ use frost_secp256k1 as frost; let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + //////////////////////////////////////////////////////////////////////////// // Key generation, Round 1 //////////////////////////////////////////////////////////////////////////// -let max_signers = 5; -let min_signers = 3; - // 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. @@ -53,16 +55,18 @@ let mut received_round1_packages = HashMap::new(); // 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 (secret_package, round1_package) = frost::keys::dkg::part1( + # // 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, secret_package); + 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 HashMap; in practice this will be @@ -99,12 +103,14 @@ let mut received_round2_packages = HashMap::new(); // 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_packages) = frost::keys::dkg::part2( - round1_secret_packages - .remove(&participant_identifier) - .unwrap(), - &received_round1_packages[&participant_identifier], - )?; + 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. @@ -142,13 +148,18 @@ let mut pubkey_packages = HashMap::new(); // 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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::part3( - &round2_secret_packages[&participant_identifier], - &received_round1_packages[&participant_identifier], - &received_round2_packages[&participant_identifier], + 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_for_participant); + pubkey_packages.insert(participant_identifier, pubkey_package); } // With its own key package and the pubkey package, each participant can now proceed diff --git a/rfcs/0001-messages.md b/rfcs/0001-messages.md deleted file mode 100644 index d130451b..00000000 --- a/rfcs/0001-messages.md +++ /dev/null @@ -1,431 +0,0 @@ -# FROST messages - -Proposes a message layout to exchange information between participants of a FROST setup using the [jubjub](https://github.com/zkcrypto/jubjub) curve. - -## Motivation - -Currently FROST library is complete for 2 round signatures with a dealer/aggregator setup. -This proposal acknowledges that specific features, additions and upgrades will need to be made when DKG is implemented. - -Assuming all participants have a FROST library available, we need to define message structures in a way that data can be exchanged between participants. The proposal is a collection of data types so each side can do all the actions needed for a real life situation. - -## Definitions - -- `dealer` - Participant who distributes the initial package to all the other participants. -- `aggregator` - Participant in charge of collecting all the signatures from the other participants and generating the final group signature. -- `signer` - Participant that will receive the initial package, sign and send the signature to the aggregator to receive the final group signature. - -Note: In this RFC we consider the above 3 participants to be different. `dealer` and `aggregator` have specific hard coded `ParticipantId`s, so for example a `dealer` can't be a `signer`. This is not a protocol limitation but a specific rule introduced in this document. - -## Guide-level explanation - -We propose a message separated in 2 parts, a header and a payload: - -```rust -/// The data required to serialize a frost message. -struct Message { - header: Header, - payload: Payload, -} -``` - -`Header` will look as follows: - -```rust -/// The data required to serialize the common header fields for every message. -/// -/// Note: the `msg_type` is derived from the `payload` enum variant. -struct Header { - version: MsgVersion, - sender: ParticipantId, - receiver: ParticipantId, -} -``` - -While `Payload` will be defined as: - -```rust -/// The data required to serialize the payload for a message. -enum Payload { - SharePackage(messages::SharePackage), - SigningCommitments(messages::SigningCommitments), - SigningPackage(messages::SigningPackage), - SignatureShare(messages::SignatureShare), - AggregateSignature(messages::AggregateSignature), -} -``` - -All the messages and new types will be defined in a new file `src/frost/messages.rs` - -## Reference-level explanation - -Here we explore in detail the header types and all the message payloads. - -### Header - -Fields of the header define new types. Proposed implementation for them is as follows: - -```rust -/// The numeric values used to identify each `Payload` variant during serialization. -#[repr(u8)] -#[non_exhaustive] -enum MsgType { - SharePackage, - SigningCommitments, - SigningPackage, - SignatureShare, - AggregateSignature, -} - -/// The numeric values used to identify the protocol version during serialization. -struct MsgVersion(u8); - -const BASIC_FROST_SERIALIZATION: MsgVersion = MsgVersion(0); - -/// The numeric values used to identify each participant during serialization. -/// -/// In the `frost` module, participant ID `0` should be invalid. -/// But in serialization, we want participants to be indexed from `0..n`, -/// where `n` is the number of participants. -/// This helps us look up their shares and commitments in serialized arrays. -/// So in serialization, we assign the dealer and aggregator the highest IDs, -/// and mark those IDs as invalid for signers. Then we serialize the -/// participants in numeric order of their FROST IDs. -/// -/// "When performing Shamir secret sharing, a polynomial `f(x)` is used to generate -/// each party’s share of the secret. The actual secret is `f(0)` and the party with -/// ID `i` will be given a share with value `f(i)`. -/// Since a DKG may be implemented in the future, we recommend that the ID `0` be declared invalid." -/// https://raw.githubusercontent.com/ZcashFoundation/redjubjub/main/zcash-frost-audit-report-20210323.pdf#d -enum ParticipantId { - /// A serialized participant ID for a signer. - /// - /// Must be less than or equal to `MAX_SIGNER_PARTICIPANT_ID`. - Signer(u64), - /// The fixed participant ID for the dealer. - Dealer, - /// The fixed participant ID for the aggregator. - Aggregator, -} - -/// The fixed participant ID for the dealer. -const DEALER_PARTICIPANT_ID: u64 = u64::MAX - 1; - -/// The fixed participant ID for the aggregator. -const AGGREGATOR_PARTICIPANT_ID: u64 = u64::MAX; - -/// The maximum `ParticipantId::Signer` in this serialization format. -/// -/// We reserve two participant IDs for the dealer and aggregator. -const MAX_SIGNER_PARTICIPANT_ID: u64 = u64::MAX - 2; -``` - -### Payloads - -Each payload defines a new message: - -```rust -/// The data required to serialize `frost::SharePackage`. -/// -/// The dealer sends this message to each signer for this round. -/// With this, the signer should be able to build a `SharePackage` and use -/// the `sign()` function. -/// -/// Note: `frost::SharePackage.public` can be calculated from `secret_share`. -struct messages::SharePackage { - /// The public signing key that represents the entire group: - /// `frost::SharePackage.group_public`. - group_public: VerificationKey, - /// This participant's secret key share: `frost::SharePackage.share.value`. - secret_share: frost::Secret, - /// The commitments to the coefficients for our secret polynomial _f_, - /// used to generate participants' key shares. Participants use these to perform - /// verifiable secret sharing. - /// Share packages that contain duplicate or missing `ParticipantId`s are invalid. - /// `ParticipantId`s must be serialized in ascending numeric order. - share_commitment: BTreeMap, -} - -/// The data required to serialize `frost::SigningCommitments`. -/// -/// Each signer must send this message to the aggregator. -/// A signing commitment from the first round of the signing protocol. -struct messages::SigningCommitments { - /// The hiding point: `frost::SigningCommitments.hiding` - hiding: frost::Commitment, - /// The binding point: `frost::SigningCommitments.binding` - binding: frost::Commitment, -} - -/// The data required to serialize `frost::SigningPackage`. -/// -/// The aggregator decides what message is going to be signed and -/// sends it to each signer with all the commitments collected. -struct messages::SigningPackage { - /// The collected commitments for each signer as an ordered map of - /// unique participant identifiers: `frost::SigningPackage.signing_commitments` - /// - /// Signing packages that contain duplicate or missing `ParticipantId`s are invalid. - /// `ParticipantId`s must be serialized in ascending numeric order. - signing_commitments: BTreeMap, - /// The message to be signed: `frost::SigningPackage.message`. - /// - /// Each signer should perform protocol-specific verification on the message. - message: Vec, -} - -/// The data required to serialize `frost::SignatureShare`. -/// -/// Each signer sends their signatures to the aggregator who is going to collect them -/// and generate a final spend signature. -struct messages::SignatureShare { - /// This participant's signature over the message: `frost::SignatureShare.signature` - signature: frost::SignatureResponse, -} - -/// The data required to serialize a successful output from `frost::aggregate()`. -/// -/// The final signature is broadcasted by the aggregator to all signers. -struct messages::AggregateSignature { - /// The aggregated group commitment: `Signature.r_bytes` returned by `frost::aggregate` - group_commitment: frost::GroupCommitment, - /// A plain Schnorr signature created by summing all the signature shares: - /// `Signature.s_bytes` returned by `frost::aggregate` - schnorr_signature: frost::SignatureResponse, -} -``` - -## Validation - -Validation is implemented to each new data type as needed. This will ensure the creation of valid messages before they are send and right after they are received. We create a trait for this as follows: - -```rust -pub trait Validate { - fn validate(&self) -> Result<&Self, MsgErr>; -} -``` - -And we implement where needed. For example, in the header, sender and receiver can't be the same: - -```rust -impl Validate for Header { - fn validate(&self) -> Result<&Self, MsgErr> { - if self.sender.0 == self.receiver.0 { - return Err(MsgErr::SameSenderAndReceiver); - } - Ok(self) - } -} -``` - -This will require to have validation error messages as: - -```rust -use thiserror::Error; - -#[derive(Clone, Error, Debug)] -pub enum MsgErr { - #[error("sender and receiver are the same")] - SameSenderAndReceiver, -} -``` - -Then to create a valid `Header` in the sender side we call: - -```rust -let header = Validate::validate(&Header { - .. -}).expect("a valid header"); -``` - -The receiver side will validate the header using the same method. Instead of panicking the error can be ignored to don't crash and keep waiting for other (potentially valid) messages. - -```rust -if let Ok(header) = msg.header.validate() { - .. -} -``` - -### Rules - -The following rules must be implemented: - -#### Header - -- `version` must be a supported version. -- `sender` and `receiver` can't be the same. -- The `ParticipantId` variants of `sender` and `receiver` must match the message type. - -#### Payloads - -- Each jubjub type must be validated during deserialization. -- `share_commitments`: - - Length must be less than or equal to `MAX_SIGNER_PARTICIPANT_ID`. - - Length must be at least `MIN_SIGNERS` (`2` signers). - - Duplicate `ParticipantId`s are invalid. This is implicit in the use of `BTreeMap` during serialization, but must be checked during deserialization. - - Commitments must be serialized in ascending numeric `ParticipantId` order. This is the order of `BTreeMap.iter` during serialization, but must be checked during deserialization. -- `signing_commitments`: - - Length must be less than or equal to `MAX_SIGNER_PARTICIPANT_ID`. - - Length must be at least `MIN_THRESHOLD` (`2` required signers). - - Signing packages that contain duplicate `ParticipantId`s are invalid. This is implicit in the use of `BTreeMap` during serialization, but must be checked during deserialization. - - Signing packages must serialize in ascending numeric `ParticipantId` order. This is the order of `BTreeMap.iter` during serialization, but must be checked during deserialization.. -- `message`: signed messages have a protocol-specific length limit. For Zcash, that limit is the maximum network protocol message length: `2^21` bytes (2 MB). - -## Serialization/Deserialization - -Each message struct needs to serialize to bytes representation before it is sent through the wire and must deserialize to the same struct (round trip) on the receiver side. We use `serde` and macro derivations (`Serialize` and `Deserialize`) to automatically implement where possible. - -This will require deriving serde in several types defined in `frost.rs`. -Manual implementation of serialization/deserialization will be located at a new mod `src/frost/serialize.rs`. - -### Byte order - -Each byte chunk specified below is in little-endian order unless is specified otherwise. - -Multi-byte integers **must not** be used for serialization, because they have different byte orders on different platforms. - -### Header - -The `Header` part of the message is 18 bytes total: - -Bytes | Field name | Data type -------|------------|----------- -1 | version | u8 -1 | msg_type | u8 -8 | sender | u64 -8 | receiver | u64 - -### Frost types - -The FROST types we will be using in the messages can be represented always as a primitive type. For serialization/deserialization purposes: - -- `Commitment` = `AffinePoint` -- `Secret` = `Scalar` -- `GroupCommitment` = `AffinePoint` -- `SignatureResponse` = `Scalar` - -### Primitive types - -`Payload`s use data types that we need to specify first. We have 3 primitive types inside the payload messages: - -#### `Scalar` - -`jubjub::Scalar` is a an alias for `jubjub::Fr`. We use `Scalar::to_bytes` and `Scalar::from_bytes` to get a 32-byte little-endian canonical representation. See https://github.com/zkcrypto/bls12_381/blob/main/src/scalar.rs#L260 and https://github.com/zkcrypto/bls12_381/blob/main/src/scalar.rs#L232 - -#### `AffinePoint` - -Much of the math in FROST is done using `jubjub::ExtendedPoint`. But for message exchange `jubjub::AffinePoint`s are a better choice, as their byte representation is smaller. - -Conversion from one type to the other is trivial: - -https://docs.rs/jubjub/0.6.0/jubjub/struct.AffinePoint.html#impl-From%3CExtendedPoint%3E -https://docs.rs/jubjub/0.6.0/jubjub/struct.ExtendedPoint.html#impl-From%3CAffinePoint%3E - -We use `AffinePoint::to_bytes` and `AffinePoint::from_bytes` to get a 32-byte little-endian canonical representation. See https://github.com/zkcrypto/jubjub/blob/main/src/lib.rs#L443 - -#### VerificationKey - -`redjubjub::VerificationKey`s can be serialized and deserialized using `<[u8; 32]>::from` and `VerificationKey::from`. See https://github.com/ZcashFoundation/redjubjub/blob/main/src/verification_key.rs#L80-L90 and https://github.com/ZcashFoundation/redjubjub/blob/main/src/verification_key.rs#L114-L121. - -### Payload - -Payload part of the message is variable in size and depends on message type. - -#### `SharePackage` - -Bytes | Field name | Data type -----------------|------------------|----------- -32 | group_public | VerificationKey -32 | secret_share | Share -1 | participants | u8 -(8+32)*participants | share_commitment | BTreeMap - -#### `SigningCommitments` - -Bytes | Field name | Data type ---------|---------------------|----------- -32 | hiding | Commitment -32 | binding | Commitment - -#### `SigningPackage` - -Bytes | Field name | Data type ------------------------|--------------------|----------- -1 | participants | u8 -(8+32+32)*participants | signing_commitments| BTreeMap -8 | message_length | u64 -message_length | message | Vec\ - - -#### `SignatureShare` - -Bytes | Field name | Data type -------|------------|----------- -32 | signature | SignatureResponse - -#### `AggregateSignature` - -Bytes | Field name | Data type -------|------------------|----------- -32 | group_commitment | GroupCommitment -32 | schnorr_signature| SignatureResponse - -## Not included - -The following are a few things this RFC is not considering: - -- The RFC does not describe implementation-specific issues - it is focused on message structure and serialization. -- Implementations using this serialization should handle missing messages using timeouts or similar protocol-specific mechanisms. - - This is particularly important for `SigningPackage`s, which only need a threshold of participants to continue. -- Messages larger than 4 GB are not supported on 32-bit platforms. -- Implementations should validate that message lengths are lower than a protocol-specific maximum length, then allocate message memory. -- Implementations should distinguish between FROST messages from different signature schemes using implementation-specific mechanisms. - -### State-Based Validation - -The following validation rules should be checked by the implementation: - -- `share_commitments`: The number of participants in each round is set by the length of `share_commitments`. - - If `sender` and `receiver` are a `ParticipantId::Signer`, they must be less than the number of participants in this round. - - The length of `signing_commitments` must be less than or equal to the number of participants in this round. -- `signing_commitments`: Signing packages that contain missing `ParticipantId`s are invalid - - Note: missing participants are supported by this serialization format. - But implementations can require all participants to fully participate in each round. - -If the implementation knows the number of key shares, it should re-check all the validation rules involving `MAX_SIGNER_PARTICIPANT_ID` using that lower limit. - -## Testing plan - -### Test Vectors - -#### Conversion on Test Vectors - -- Test conversion from `frost` to `message` on a test vector - 1. Implement the Rust `message` struct - 2. Implement conversion from and to the `frost` type - 3. Do a round-trip test from `frost` to `message` on a test vector -- Test conversion from `message` to bytes on a test vector - 1. Implement conversion from and to the `message` type - 2. Do a round-trip test from `message` to bytes on a test vector - -#### Signing Rounds on Test Vectors - -- Test signing using `frost` types on a test vector - 1. Implement a single round of `frost` signing using a test vector -- Test signing using `message` types on a test vector -- Test signing using byte vectors on a test vector - -### Property Tests - -#### Conversion Property Tests - -- Create property tests for each message - - Test round-trip conversion from `frost` to `message` types - - Test round-trip serialization and deserialization for each `message` type - -#### Signing Round Property Tests - -- Create property tests for signing rounds - - Test a signing round with `frost` types - - Test a signing round with `message` types - - Test a signing round with byte vectors