diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index d8c8d6b3..47ece33b 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -44,4 +44,4 @@ jobs: run: cargo llvm-cov report --lcov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' --output-path lcov.info - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v3.1.4 + uses: codecov/codecov-action@v4.0.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f71da697..e547fcd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -143,7 +143,7 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v4.1.1 - - uses: reviewdog/action-actionlint@v1.40.0 + - uses: reviewdog/action-actionlint@v1.41.0 with: level: warning fail_on_error: false diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 883622c1..a110f6d8 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into main - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 with: # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml config-name: release-drafter.yml diff --git a/README.md b/README.md index a1c35cb8..0101e837 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,33 @@ Refer to the [ZF FROST book](https://frost.zfnd.org/). ## Status ⚠ The FROST specification is not yet finalized, though no significant changes are -expected at this point. This code base has been audited by NCC. The APIs and -types in `frost-core` are subject to change during the release candidate phase, -and will follow SemVer guarantees after 1.0.0. +expected at this point. This code base has been partially audited by NCC, see +below for details. The APIs and types in the crates contained in this repository +follow SemVer guarantees. + +### NCC Audit + +NCC performed [an +audit](https://research.nccgroup.com/2023/10/23/public-report-zcash-frost-security-assessment/) +of the v0.6.0 release (corresponding to commit 5fa17ed) of the following crates: + +- frost-core +- frost-ed25519 +- frost-ed448 +- frost-p256 +- frost-secp256k1 +- frost-ristretto255 + +This includes key generation (both trusted dealer and DKG) and FROST signing. +This does not include rerandomized FROST. + +The parts of the +[`Ed448-Goldilocks`](https://github.com/crate-crypto/Ed448-Goldilocks) +dependency that are used by `frost-ed448` were also in scope, namely the +elliptic curve operations. + +All issues identified in the audit were addressed by us and reviewed by NCC. + ## Usage diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 4cf983c7..b7cca859 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Trusted Dealer Key Generation](tutorial/trusted-dealer.md) - [Signing](tutorial/signing.md) - [Distributed Key Generation](tutorial/dkg.md) + - [Key Resharing](tutorial/resharing.md) - [User Documentation](user.md) - [Serialization Format](user/serialization.md) - [FROST with Zcash](zcash.md) diff --git a/book/src/tutorial/importing.md b/book/src/tutorial/importing.md index 1988bbed..7fd3c563 100644 --- a/book/src/tutorial/importing.md +++ b/book/src/tutorial/importing.md @@ -6,7 +6,7 @@ Add to your `Cargo.toml` file: ``` [dependencies] -frost-ristretto255 = "1.0.0-rc.0" +frost-ristretto255 = "1.0.0" ``` ## Handling errors diff --git a/book/src/tutorial/resharing.md b/book/src/tutorial/resharing.md new file mode 100644 index 00000000..0cf859c9 --- /dev/null +++ b/book/src/tutorial/resharing.md @@ -0,0 +1,124 @@ +# Key Resharing + +_Resharing_ is the process of dynamically re-generating the shares of a FROST signing group, without recovering the group's master private key. This is effectively like repeating [the Distributed Key Generation process](./dkg.html) so that new shares are distributed to each signer, except the group **retains the same group verifying key.** Signers can verify their new shares are valid for the same group verifying key, and any invalid contributions can be identified just like during a FROST signing session. + +In so doing, signing groups can achieve a number of interesting use cases: + +- [Revoking exposed shares](#revoking-exposed-shares) +- Protection against [Mobile Adversaries](#mobile-adversaries) +- [Changing the signing group and threshold](#changing-the-group-and-threshold) + +## Revoking Exposed Shares + +Consider a case where one FROST signer's share was accidentally exposed publicly - For example, published on social media, or revealed through secret nonce reuse. The group's signing/security threshold will have decreased by one share (since the exposed share is known to everyone). The signing group would probably want some way to revoke that share, and optionally issue a new share to the signer who exposed their share, thereby recovering their group's desired security properties. + +_Resharing_ provides a simple mechanism which accomplishes this. Every time the resharing protocol is executed, the signers overwrite their old signing shares with new shares which are _incompatible_ with their old ones. Assuming the other signers' shares remained secret until they were erased, the exposed share is now unusable. + +### Example + +Consider a 3-of-4 threshold signing group: Alice, Bob, Carol, and Dave, with shares `a1`, `b1`, `c1`, and `d1` respectively. + +``` +┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ +│ Alice │ │ Bob │ │ Carol │ │ Dave │ +│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ +│ │share_a1│ │ │ │share_b1│ │ │ │share_c1│ │ │ │share_d1│ │ +│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ +└────────────┘ └────────────┘ └────────────┘ └────────────┘ +``` + +Bob exposes his share `b1` by posting it to his MySpace page. How silly of Bob. Now his share `b1` is known by every other signer, and by the rest of the internet at large. + +``` +┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ +│ Alice │ │ Bob │ │ Carol │ │ Dave │ +│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ +│ │share_a1│ │ │ │share_b1│ │ │ │share_c1│ │ │ │share_d1│ │ +│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ +└────────────┘ └────────────┘ └────────────┘ └────────────┘ + + ┌──────────────────────────────────────┐ + │ Public Knowledge │ + │ ┌────────┐ │ + │ │share_b1│ │ + │ └────────┘ │ + └──────────────────────────────────────┘ +``` + +The signers agree to execute a resharing procedure, and issue Bob a new share in the process. Since Bob's `b1` share is public now, only a minimum of 2 out of the 4 signers need to be online and available to execute the resharing, but for practical reasons, it is best for all signers to participate. + +This process results in four new shares, `a2`, `b2`, `c2`, and `d2` distributed to Alice, Bob, Carol and Dave respectively. These shares are **incompatible** with the four original shares `[a1, b1, c1, d1]`. Shares from different key-generation or resharing runs **cannot** be used together. For instance, the set of shares `[a2, b1, c2]` would **not** be sufficient to sign on behalf of the FROST group. + +Upon receiving their new shares and acknowledging their validity, signers securely erase the old shares. This step is important but unfortunately not verifiable. Nothing prevents signers from keeping their old shares. If everyone behaves honestly though, the exposed share `b1` is rendered useless, because the other three shares `a1`, `c1`, and `d1` are now permanently erased and unknowable. + +``` +┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ +│ Alice │ │ Bob │ │ Carol │ │ Dave │ +│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ +│ │xxxxxxxx│ │ │ │xxxxxxxx│ │ │ │xxxxxxxx│ │ │ │xxxxxxxx│ │ +│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ +│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ +│ │share_a2│ │ │ │share_b2│ │ │ │share_c2│ │ │ │share_d2│ │ +│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ +└────────────┘ └────────────┘ └────────────┘ └────────────┘ + + ┌──────────────────────────────────────┐ + │ Public Knowledge │ + │ ┌────────┐ │ + │ │share_b1│ <--- useless │ + │ └────────┘ │ + └──────────────────────────────────────┘ +``` + +## Mobile Adversaries + +A _mobile adversary_ is a hypothetical term denoting an attacker who _corrupts_ some fraction of signers _slowly over time,_ such as by infecting their computers with a virus which only spreads to one signer at a time. However, a corrupted signer is not guaranteed to _stay_ corrupted. They might realize their system is infected and change over to a new computer, or they might reinstall their operating system. + +As time goes on, the mobile adversary can _corrupt_ more of the signing group. Although some signers might _un-corrupt_ themselves, the adversary has still learned their secret share through the virus, and thus inches closer to breaking the security threshold `t` of the signing group. Once the adversary has corrupted `t` signers at least once, they have learned enough shares to sign arbitrary messages on behalf of the group. + +_Resharing_ allows a signing group to defend themselves against mobile adversaries. By resharing on a regular basis, the group ensures any shares exposed to mobile adversaries are revoked. The adversary must then corrupt `t` or more signers _all at once,_ which is much harder. Provided the group executes the resharing protocol frequently enough, and signers overwrite their old signing shares each time, the mobile adversary will have a much harder time learning enough compatible shares to effectively attack the group. + +## Changing the Group and Threshold + +The recipients of shares from a resharing execution do not necessarily need to be the same as the original group of signers. It is possible for resharing to be used to intentionally exclude certain members of the signing group by effectively revoking their shares. + +Equally, resharing can be used to add new signers into the group. Although if the group only wants to _add_ new members without removing any old signers, then using _repairable secret sharing_ is probably a simpler approach. + +Perhaps most interestingly though, resharing allows the participants to decide on a new _group signing threshold_ which applies to the newly issued shares. Threshold modification has some gotchas though. The new threshold, denoted `t'`, only applies to the shares issued by the relevant resharing execution. The old shares from before the resharing are still valid and retain the old threshold, denoted `t`. Unless deleted, they could still be used together. + +Thus, using resharing to modify the threshold should be used cautiously, and with the assumption that signers _could_ choose to retain their old shares. Reducing `t` is generally less problematic than increasing it, because new shares with a lower threshold will carry more signing power than old shares, and so signers have less incentive to retain the old shares. + +# A Resharing Run + +Resharing is split into three logical steps: + +1. Broadcast commitment +2. Send subshares +3. Reconstruct new share +4. (optional) ACK and delete old share + +## Broadcast Commitment + +A public commitment must be sent over a [**broadcast channel**](/terminology.html#broadcast-channel) to all the recipient peers, who will be part of the new group after resharing. + +## Send Subshares + +Subshares, which one could think of as "shares-of-a-share", are sent to the recipients over [an authenticated & confidential channel](/terminology.html#peer-to-peer-channel). + +## Reconstruct New Share + +The recipients receive commitments and subshares from the resharers. They can verify each subshare is consistent with its sender's commitment, and also compute a new signing share from the subshares. + +If the recipient knows the public verifying key of the group they are joining, the recipient can verify the resulting share they reconstructed is valid for that group key. + +If the recipient knows the public verifying _shares_ of the individual resharers, they can assign blame to any resharer who may have provided them with an invalid subshare and commitment pair. + +## (optional) ACK and Delete Old Share + +Once all recipients have acknowledged they received and reconstructed a new set of valid signing shares, then the resharers can erase their old signing shares. + +```admonish danger +It is important that all recipients acknowledge successful resharing before any signing shares are erased. Premature share erasure can result in a ['Forget-and-Forgive'](https://iacr.org/submit/files/slides/2021/rwc/rwc2021/31/slides.pdf) attack, where a single malicious signer can convince some of the group to overwrite their old shares by giving providing valid subshares, but block others from finishing the procedure by providing them with _invalid_ subshares. + +This attack _splits_ the group in two: Those who have _new_ shares and those who have _old_ shares. If `t > n/2` (i.e. if the threshold is greater than half the group size), this results in a deadlock where not enough compatible signing shares exist anymore for the group to recover. +``` diff --git a/frost-core/CHANGELOG.md b/frost-core/CHANGELOG.md index 801d450a..4821c475 100644 --- a/frost-core/CHANGELOG.md +++ b/frost-core/CHANGELOG.md @@ -4,9 +4,19 @@ Entries are listed in reverse chronological order. ## Unreleased - ## Released +## 1.0.0 + +* Exposed the `SigningKey::from_scalar()` and `to_scalar()` methods. This + helps interoperability with other implementations. +* Exposed the `SigningNonces::from_nonces()` method to allow it to be + deserialized. +* Fixed bug that prevented deserialization with in some cases (e.g. JSON + containing escape codes). +* Added `new()` methods for `VerifirableSecretSharingCommitment` and + `CoefficientCommitment`. + ## 1.0.0-rc.0 * The `frost-core::frost` module contents were merged into `frost-core`, thus diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index 7b1d248d..0dcaa8b6 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", diff --git a/frost-core/src/keys.rs b/frost-core/src/keys.rs index 533cb13f..2acc9c30 100644 --- a/frost-core/src/keys.rs +++ b/frost-core/src/keys.rs @@ -29,6 +29,7 @@ use super::compute_lagrange_coefficient; pub mod dkg; pub mod repairable; +pub mod resharing; /// Sum the commitments from all participants in a distributed key generation /// run into a single group commitment. diff --git a/frost-core/src/keys/repairable.rs b/frost-core/src/keys/repairable.rs index 169e0ab8..931e3a83 100644 --- a/frost-core/src/keys/repairable.rs +++ b/frost-core/src/keys/repairable.rs @@ -107,7 +107,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-core/src/keys/resharing.rs b/frost-core/src/keys/resharing.rs new file mode 100644 index 00000000..0e34ec80 --- /dev/null +++ b/frost-core/src/keys/resharing.rs @@ -0,0 +1,240 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::{ + compute_lagrange_coefficient, Ciphersuite, CryptoRng, Error, Field, Group, Identifier, RngCore, + Scalar, +}; + +use super::{ + evaluate_vss, split, validate_num_of_signers, CoefficientCommitment, IdentifierList, + KeyPackage, PublicKeyPackage, SecretShare, SigningKey, SigningShare, + VerifiableSecretSharingCommitment, VerifyingShare, +}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, SecretSubshare>, Error> { + let (subshares, _) = split( + &SigningKey::from_scalar(share_i.0), + new_idents.len() as u16, + new_threshold, + IdentifierList::Custom(new_idents), + rng, + )?; + + Ok(subshares) +} + +/// TODO docs +pub fn verify_commitment( + sender_ident: &Identifier, + old_pubkeys: &PublicKeyPackage, + commitment: &VerifiableSecretSharingCommitment, + new_threshold: u16, +) -> Result<(), Error> { + // Ensure each subshare is from a member of the group. + let verifying_share = old_pubkeys + .verifying_shares + .get(sender_ident) + .ok_or(Error::UnknownIdentifier)?; + + // Constant term of the commitment MUST be the same as the sender's own + // public share. If this fails, the sender used the wrong share to generate + // their commitment. + if commitment.coefficients()[0].value() != verifying_share.to_element() { + return Err(Error::IncorrectCommitment)?; // TODO add culprit + } + + // Every peer's resharing polynomial must have degree `t' - 1`. + if commitment.coefficients().len() != new_threshold as usize { + return Err(Error::InvalidCoefficients); // TODO add culprit + } + + Ok(()) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, SecretSubshare>, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + validate_num_of_signers(new_threshold, new_idents.len() as u16)?; + for (sender_ident, subshare) in received_subshares.into_iter() { + verify_commitment( + &sender_ident, + old_pubkeys, + &subshare.commitment, + new_threshold, + )?; + } + + let old_idents: BTreeSet> = received_subshares.keys().copied().collect(); + let lagrange_coefficients: BTreeMap, Scalar> = old_idents + .iter() + .map(|&id| -> Result<(Identifier, Scalar), Error> { + let l = compute_lagrange_coefficient(&old_idents, None, id)?; + Ok((id, l)) + }) + .collect::>>()?; + + let group_pubkey = received_subshares + .into_iter() + .map(|(id, subshare)| { + subshare.commitment.coefficients()[0].value() * lagrange_coefficients[id] + }) + .reduce(|sum, term| sum + term) + .ok_or(Error::IncorrectNumberOfShares)?; // At least one subshare is required. + + // The pubkeys participating in resharing must represent at least the old + // threshold `t` of the group. The interpolated pubkey will not match here + // unless that threshold is met. + if group_pubkey != old_pubkeys.verifying_key.to_element() { + return Err(Error::IncorrectNumberOfShares); + } + + let mut new_share_sum = ::Field::zero(); + + for (sender_ident, subshare) in received_subshares.into_iter() { + // Verify the subshare against the commitment. + // s_{ij} * G == G * ( s_i + a_1*j + a_2 * j^2 + ... + a_{t'-1} * j^{t'-1} ) + let s = subshare.signing_share.to_scalar(); + if C::Group::generator() * s != evaluate_vss(our_ident, &subshare.commitment) { + return Err(Error::InvalidSecretShare); // TODO add culprit + } + + // The new share is computed by interpolating the constant coefficient of a + // new polynomial generated jointly by the signers who participated in resharing. + new_share_sum = new_share_sum + s * lagrange_coefficients[sender_ident]; + } + + let new_signing_share = SigningShare(new_share_sum); + + // The group's new public polynomial coefficients can be computed by treating commitment + // coefficients as polynomial evaluations and interpolating the resulting function. + // See Step 8 here: https://conduition.io/cryptography/shamir-resharing/#Resharing + let new_group_commit_coeffs: Vec> = (0..new_threshold as usize) + .map(|k| { + received_subshares + .iter() + .fold(C::Group::identity(), |sum, (id, subshare)| { + sum + subshare.commitment.coefficients()[k].value() * lagrange_coefficients[id] + }) + }) + .map(CoefficientCommitment) + .collect(); + + // The new group commitment should match the group pubkey. + if new_group_commit_coeffs[0].value() != old_pubkeys.verifying_key.to_element() { + return Err(Error::IncorrectCommitment); + } + + let new_group_commitment = VerifiableSecretSharingCommitment(new_group_commit_coeffs); + + let new_verifying_shares: BTreeMap, VerifyingShare> = new_idents + .into_iter() + .map(|&id| (id, VerifyingShare(evaluate_vss(id, &new_group_commitment)))) + .collect(); + + // Our identifier must be one of the intended resharing recipients. + let new_verifying_share = new_verifying_shares + .get(&our_ident) + .ok_or(Error::UnknownIdentifier)? + .clone(); + + // Sanity check; our new share should be valid for the new commitment. + if C::Group::generator() * new_share_sum != new_verifying_share.to_element() { + return Err(Error::InvalidSecretShare); + } + + let new_pubkey_pkg = PublicKeyPackage::new(new_verifying_shares, old_pubkeys.verifying_key); + + let new_secret_key_package = KeyPackage::new( + our_ident, + new_signing_share, + new_verifying_share, + old_pubkeys.verifying_key, + new_threshold, + ); + + Ok((new_secret_key_package, new_pubkey_pkg)) +} diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs index 6d165835..26d812f2 100644 --- a/frost-core/src/signing_key.rs +++ b/frost-core/src/signing_key.rs @@ -58,7 +58,6 @@ where } /// Creates a SigningKey from a scalar. - #[cfg(feature = "internals")] pub fn from_scalar( scalar: <<::Group as Group>::Field as Field>::Scalar, ) -> Self { @@ -66,7 +65,6 @@ where } /// Return the underlying scalar. - #[cfg(feature = "internals")] pub fn to_scalar(self) -> <<::Group as Group>::Field as Field>::Scalar { self.scalar } diff --git a/frost-core/src/tests.rs b/frost-core/src/tests.rs index d2b1b712..191221f7 100644 --- a/frost-core/src/tests.rs +++ b/frost-core/src/tests.rs @@ -8,6 +8,7 @@ pub mod coefficient_commitment; pub mod helpers; pub mod proptests; pub mod repairable; +pub mod resharing; pub mod vectors; pub mod vectors_dkg; pub mod vss_commitment; diff --git a/frost-core/src/tests/resharing.rs b/frost-core/src/tests/resharing.rs new file mode 100644 index 00000000..a3f9e546 --- /dev/null +++ b/frost-core/src/tests/resharing.rs @@ -0,0 +1,145 @@ +//! Test for Verifiable Secret Redistribution (AKA resharing). + +use std::collections::BTreeMap; + +use rand_core::{CryptoRng, RngCore}; + +use crate as frost; +use crate::{ + keys::{ + resharing::{reshare_step_1, reshare_step_2, SecretSubshare}, + PublicKeyPackage, SecretShare, + }, + Ciphersuite, Identifier, +}; + +/// Check correctness of the verifiable secret redistribution protocol. +pub fn check_vsr(mut rng: R) { + // Generate old keys and shares. + let max_signers = 5; + let old_min_signers = 3; + let (old_shares, old_pubkeys): (BTreeMap, SecretShare>, PublicKeyPackage) = + frost::keys::generate_with_dealer( + max_signers, + old_min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + + // Signer 1, 2, and 4 will participate in resharing. + let helper_1 = &old_shares[&Identifier::try_from(1).unwrap()]; + let helper_2 = &old_shares[&Identifier::try_from(2).unwrap()]; + let helper_4 = &old_shares[&Identifier::try_from(4).unwrap()]; + + // They will reshare the key amongst themselves, plus new signer 5. + // Signer 3 will be excluded. + let new_signer_5_ident = Identifier::try_from(5).unwrap(); + let new_signer_idents = [ + helper_1.identifier, + helper_2.identifier, + helper_4.identifier, + new_signer_5_ident, + ]; + + // The threshold will be changed from 3 to 2. + let new_min_signers = 2; + + // Each helper generates their random coefficients and commitments. + let helper_1_subshares = reshare_step_1( + &helper_1.signing_share, + &mut rng, + new_min_signers, + &new_signer_idents, + ) + .expect("error computing resharing step 1 for helper 1"); + + let helper_2_subshares = reshare_step_1( + &helper_2.signing_share, + &mut rng, + new_min_signers, + &new_signer_idents, + ) + .expect("error computing resharing step 1 for helper 2"); + + let helper_4_subshares = reshare_step_1( + &helper_4.signing_share, + &mut rng, + new_min_signers, + &new_signer_idents, + ) + .expect("error computing resharing step 1 for helper 4"); + + let all_subshares = BTreeMap::from([ + (helper_1.identifier, helper_1_subshares), + (helper_2.identifier, helper_2_subshares), + (helper_4.identifier, helper_4_subshares), + ]); + + // Sort the subshares into a map of `recipient => sender => subshare`. + let received_subshares = new_signer_idents + .into_iter() + .map(|recipient_id| { + let received_subshares = all_subshares + .iter() + .map(|(&sender_id, sender_shares)| { + (sender_id, sender_shares[&recipient_id].clone()) + }) + .collect::>(); + (recipient_id, received_subshares) + }) + .collect::, BTreeMap, SecretSubshare>>>(); + + // Recipients of the resharing can now validate and compute their new shares. + + let (new_seckeys_1, new_pubkeys_1) = reshare_step_2( + helper_1.identifier, + &old_pubkeys, + new_min_signers, + &new_signer_idents, + &received_subshares[&helper_1.identifier], + ) + .expect("error computing reshared share for signer 1"); + + let (new_seckeys_2, new_pubkeys_2) = reshare_step_2( + helper_2.identifier, + &old_pubkeys, + new_min_signers, + &new_signer_idents, + &received_subshares[&helper_2.identifier], + ) + .expect("error computing reshared share for signer 2"); + + let (new_seckeys_4, new_pubkeys_4) = reshare_step_2( + helper_4.identifier, + &old_pubkeys, + new_min_signers, + &new_signer_idents, + &received_subshares[&helper_4.identifier], + ) + .expect("error computing reshared share for signer 4"); + + let (new_seckeys_5, new_pubkeys_5) = reshare_step_2( + new_signer_5_ident, + &old_pubkeys, + new_min_signers, + &new_signer_idents, + &received_subshares[&new_signer_5_ident], + ) + .expect("error computing reshared share for signer 5"); + + // all signers should compute the same group pubkeys. + assert_eq!(new_pubkeys_1, new_pubkeys_2); + assert_eq!(new_pubkeys_1, new_pubkeys_4); + assert_eq!(new_pubkeys_1, new_pubkeys_5); + assert_eq!(new_seckeys_1.verifying_key, new_seckeys_2.verifying_key); + assert_eq!(new_seckeys_1.verifying_key, new_seckeys_4.verifying_key); + assert_eq!(new_seckeys_1.verifying_key, new_seckeys_5.verifying_key); + + // The new pubkey package should be the same group key as the old one, + // but with new coefficients and shares. + assert_eq!(new_pubkeys_1.verifying_key, old_pubkeys.verifying_key); + assert_ne!(new_pubkeys_1.verifying_shares, old_pubkeys.verifying_shares); + + assert_eq!(new_seckeys_1.min_signers, new_min_signers); +} diff --git a/frost-ed25519/Cargo.toml b/frost-ed25519/Cargo.toml index 86832f7c..4bd685af 100644 --- a/frost-ed25519/Cargo.toml +++ b/frost-ed25519/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -23,17 +23,17 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -curve25519-dalek = { version = "=4.1.1", features = ["rand_core"] } +curve25519-dalek = { version = "=4.1.2", features = ["rand_core"] } document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0" } +frost-core = { path = "../frost-core", version = "1.0.0" } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } rand_core = "0.6" sha2 = "0.10.2" [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } ed25519-dalek = "2.0.0" insta = { version = "1.31.0", features = ["yaml"] } hex = "0.4.3" diff --git a/frost-ed25519/src/keys/repairable.rs b/frost-ed25519/src/keys/repairable.rs index deb5a833..f7cc6eb2 100644 --- a/frost-ed25519/src/keys/repairable.rs +++ b/frost-ed25519/src/keys/repairable.rs @@ -44,7 +44,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-ed25519/src/keys/resharing.rs b/frost-ed25519/src/keys/resharing.rs new file mode 100644 index 00000000..46621f53 --- /dev/null +++ b/frost-ed25519/src/keys/resharing.rs @@ -0,0 +1,106 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::BTreeMap; + +use crate::Error; +use crate::{frost, CryptoRng, Identifier, RngCore}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare, SigningShare}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, Error> { + frost::keys::resharing::reshare_step_1(share_i, rng, new_threshold, new_idents) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::resharing::reshare_step_2( + our_ident, + old_pubkeys, + new_threshold, + new_idents, + received_subshares, + ) +} diff --git a/frost-ed25519/tests/integration_tests.rs b/frost-ed25519/tests/integration_tests.rs index 1421079a..c20a025d 100644 --- a/frost-ed25519/tests/integration_tests.rs +++ b/frost-ed25519/tests/integration_tests.rs @@ -64,6 +64,15 @@ fn check_rts() { frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_vsr() { + // let rng = thread_rng(); + use rand::SeedableRng; + let rng = rand::rngs::StdRng::seed_from_u64(0); + + frost_core::tests::resharing::check_vsr::(rng); +} + #[test] fn check_sign_with_dealer() { let rng = thread_rng(); diff --git a/frost-ed448/Cargo.toml b/frost-ed448/Cargo.toml index 6463229b..dcae381d 100644 --- a/frost-ed448/Cargo.toml +++ b/frost-ed448/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -24,15 +24,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" ed448-goldilocks = { version = "0.9.0" } -frost-core = { path = "../frost-core", version = "1.0.0-rc.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0" } +frost-core = { path = "../frost-core", version = "1.0.0" } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } rand_core = "0.6" sha3 = "0.10.6" [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } lazy_static = "1.4" insta = { version = "1.31.0", features = ["yaml"] } hex = "0.4.3" diff --git a/frost-ed448/src/keys/repairable.rs b/frost-ed448/src/keys/repairable.rs index b44709fc..e05a05e7 100644 --- a/frost-ed448/src/keys/repairable.rs +++ b/frost-ed448/src/keys/repairable.rs @@ -44,7 +44,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-ed448/src/keys/resharing.rs b/frost-ed448/src/keys/resharing.rs new file mode 100644 index 00000000..46621f53 --- /dev/null +++ b/frost-ed448/src/keys/resharing.rs @@ -0,0 +1,106 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::BTreeMap; + +use crate::Error; +use crate::{frost, CryptoRng, Identifier, RngCore}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare, SigningShare}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, Error> { + frost::keys::resharing::reshare_step_1(share_i, rng, new_threshold, new_idents) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::resharing::reshare_step_2( + our_ident, + old_pubkeys, + new_threshold, + new_idents, + received_subshares, + ) +} diff --git a/frost-ed448/tests/integration_tests.rs b/frost-ed448/tests/integration_tests.rs index 3409a7e0..814921c1 100644 --- a/frost-ed448/tests/integration_tests.rs +++ b/frost-ed448/tests/integration_tests.rs @@ -64,6 +64,15 @@ fn check_rts() { frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_vsr() { + // let rng = thread_rng(); + use rand::SeedableRng; + let rng = rand::rngs::StdRng::seed_from_u64(0); + + frost_core::tests::resharing::check_vsr::(rng); +} + #[test] fn check_sign_with_dealer() { let rng = thread_rng(); diff --git a/frost-p256/Cargo.toml b/frost-p256/Cargo.toml index 276ca65d..f0dd0786 100644 --- a/frost-p256/Cargo.toml +++ b/frost-p256/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -25,15 +25,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" p256 = { version = "0.13.0", features = ["hash2curve"] } -frost-core = { path = "../frost-core", version = "1.0.0-rc.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0" } +frost-core = { path = "../frost-core", version = "1.0.0" } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } rand_core = "0.6" sha2 = "0.10.2" [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = "0.4.3" lazy_static = "1.4" diff --git a/frost-p256/src/keys/repairable.rs b/frost-p256/src/keys/repairable.rs index 310a26f7..f26a52ea 100644 --- a/frost-p256/src/keys/repairable.rs +++ b/frost-p256/src/keys/repairable.rs @@ -44,7 +44,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-p256/src/keys/resharing.rs b/frost-p256/src/keys/resharing.rs new file mode 100644 index 00000000..46621f53 --- /dev/null +++ b/frost-p256/src/keys/resharing.rs @@ -0,0 +1,106 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::BTreeMap; + +use crate::Error; +use crate::{frost, CryptoRng, Identifier, RngCore}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare, SigningShare}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, Error> { + frost::keys::resharing::reshare_step_1(share_i, rng, new_threshold, new_idents) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::resharing::reshare_step_2( + our_ident, + old_pubkeys, + new_threshold, + new_idents, + received_subshares, + ) +} diff --git a/frost-p256/tests/integration_tests.rs b/frost-p256/tests/integration_tests.rs index f2573353..d7b1ac77 100644 --- a/frost-p256/tests/integration_tests.rs +++ b/frost-p256/tests/integration_tests.rs @@ -64,6 +64,15 @@ fn check_rts() { frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_vsr() { + // let rng = thread_rng(); + use rand::SeedableRng; + let rng = rand::rngs::StdRng::seed_from_u64(0); + + frost_core::tests::resharing::check_vsr::(rng); +} + #[test] fn check_sign_with_dealer() { let rng = thread_rng(); diff --git a/frost-rerandomized/Cargo.toml b/frost-rerandomized/Cargo.toml index 32684ea5..71c37335 100644 --- a/frost-rerandomized/Cargo.toml +++ b/frost-rerandomized/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = ["Deirdre Connolly ", "Chelsea Komlo ", "Conrado Gouvea "] readme = "README.md" @@ -22,7 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] derive-getters = "0.3.0" document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["internals"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["internals"] } rand_core = "0.6" [dev-dependencies] diff --git a/frost-ristretto255/Cargo.toml b/frost-ristretto255/Cargo.toml index 38c8df14..a886f8e8 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" # - Update html_root_url # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = ["Deirdre Connolly ", "Chelsea Komlo ", "Conrado Gouvea "] readme = "README.md" license = "MIT OR Apache-2.0" @@ -19,17 +19,17 @@ features = ["serde"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -curve25519-dalek = { version = "=4.1.1", features = ["serde", "rand_core"] } +curve25519-dalek = { version = "=4.1.2", features = ["serde", "rand_core"] } document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0" } +frost-core = { path = "../frost-core", version = "1.0.0" } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } rand_core = "0.6" sha2 = "0.10.2" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = "0.4.3" lazy_static = "1.4" diff --git a/frost-ristretto255/src/keys/repairable.rs b/frost-ristretto255/src/keys/repairable.rs index a303828d..12e77aaa 100644 --- a/frost-ristretto255/src/keys/repairable.rs +++ b/frost-ristretto255/src/keys/repairable.rs @@ -44,7 +44,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-ristretto255/src/keys/resharing.rs b/frost-ristretto255/src/keys/resharing.rs new file mode 100644 index 00000000..46621f53 --- /dev/null +++ b/frost-ristretto255/src/keys/resharing.rs @@ -0,0 +1,106 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::BTreeMap; + +use crate::Error; +use crate::{frost, CryptoRng, Identifier, RngCore}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare, SigningShare}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, Error> { + frost::keys::resharing::reshare_step_1(share_i, rng, new_threshold, new_idents) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::resharing::reshare_step_2( + our_ident, + old_pubkeys, + new_threshold, + new_idents, + received_subshares, + ) +} diff --git a/frost-ristretto255/src/lib.rs b/frost-ristretto255/src/lib.rs index eabb403d..a154a2b3 100644 --- a/frost-ristretto255/src/lib.rs +++ b/frost-ristretto255/src/lib.rs @@ -306,6 +306,7 @@ pub mod keys { pub mod dkg; pub mod repairable; + pub mod resharing; } /// FROST(ristretto255, SHA-512) Round 1 functionality and types. diff --git a/frost-ristretto255/tests/integration_tests.rs b/frost-ristretto255/tests/integration_tests.rs index bacc9fb4..8830c643 100644 --- a/frost-ristretto255/tests/integration_tests.rs +++ b/frost-ristretto255/tests/integration_tests.rs @@ -64,6 +64,15 @@ fn check_rts() { frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_vsr() { + // let rng = thread_rng(); + use rand::SeedableRng; + let rng = rand::rngs::StdRng::seed_from_u64(0); + + frost_core::tests::resharing::check_vsr::(rng); +} + #[test] fn check_sign_with_dealer() { let rng = thread_rng(); diff --git a/frost-secp256k1/Cargo.toml b/frost-secp256k1/Cargo.toml index bed22b35..30e726a3 100644 --- a/frost-secp256k1/Cargo.toml +++ b/frost-secp256k1/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" # When releasing to crates.io: # - Update CHANGELOG.md # - Create git tag. -version = "1.0.0-rc.0" +version = "1.0.0" authors = [ "Deirdre Connolly ", "Chelsea Komlo ", @@ -23,16 +23,16 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] document-features = "0.2.7" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0" } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0" } +frost-core = { path = "../frost-core", version = "1.0.0" } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0" } k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"] } rand_core = "0.6" sha2 = "0.10.2" [dev-dependencies] criterion = "0.5" -frost-core = { path = "../frost-core", version = "1.0.0-rc.0", features = ["test-impl"] } -frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0-rc.0", features = ["test-impl"] } +frost-core = { path = "../frost-core", version = "1.0.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "1.0.0", features = ["test-impl"] } insta = { version = "1.31.0", features = ["yaml"] } hex = "0.4.3" lazy_static = "1.4" diff --git a/frost-secp256k1/src/keys/repairable.rs b/frost-secp256k1/src/keys/repairable.rs index 01bb964d..d5e16e80 100644 --- a/frost-secp256k1/src/keys/repairable.rs +++ b/frost-secp256k1/src/keys/repairable.rs @@ -44,7 +44,7 @@ pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { /// Step 3 of RTS /// /// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` -/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// is made up of the `identifier` and `commitment` of the `participant` as well as the /// `value` which is the `SigningShare`. pub fn repair_share_step_3( sigmas: &[Scalar], diff --git a/frost-secp256k1/src/keys/resharing.rs b/frost-secp256k1/src/keys/resharing.rs new file mode 100644 index 00000000..46621f53 --- /dev/null +++ b/frost-secp256k1/src/keys/resharing.rs @@ -0,0 +1,106 @@ +//! Dynamic resharing of FROST signing keys. +//! +//! Implements [Wang's Verifiable Secret Resharing (VSR) Scheme +#![doc = "](https://www.semanticscholar.org/paper/Verifiable-Secret-Redistribution\ +-for-Threshold-Wong-Wang/48d248779002b0015bdb99841a43395b526d5f8e)."] +//! FROST signing shares can be periodically rotated among signers to +//! protect against mobile and active adversaries. This allows old shares +//! to be 'revoked' (although only in a soft manner) and replaced with new shares. +//! +//! As a byproduct, resharing allows signers to change parameters of their +//! signing group, including setting a new threshold, changing identifiers, +//! adding new signers or excluding old signers from the new group of shares. +//! Resharing can be done even if some signers are offline; as long as the +//! signing threshold is met, the joint secret can be redistributed with new +//! shares and potentially a new threshold. +//! +//! Shares issued from before and after the resharing are mutually incompatible, +//! so it is imperative that at least the one threshold-subset of signers ACK +//! the resharing as successful before anyone deletes their old shares. See +//! [`reshare_step_2`] for more info. +//! +//! After a resharing occurs, the old shares are still usable. Normally, signers +//! are advised to delete their old shares, but nothing prevents them from keeping +//! the outdated shares either by maliciousness or through honest mistake. +//! +//! Downstream consumers should consider how inactive signers will be notified +//! about a resharing which occurrs while they are offline. + +use std::collections::BTreeMap; + +use crate::Error; +use crate::{frost, CryptoRng, Identifier, RngCore}; + +use super::{KeyPackage, PublicKeyPackage, SecretShare, SigningShare}; + +/// A subshare of a secret share. This contains the same data +/// as a [`SecretShare`], except it is actually a share of a share, +/// used in the process of resharing. +pub type SecretSubshare = SecretShare; + +/// Split a secret signing share into a set of secret subshares (shares of a share). +/// +/// `share_i` is our FROST signing share, which will be split into subshares. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is a list of identifiers for peers to whom the secret subshares +/// will be distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// The resulting output maps peers' identifiers to the subshare which they should +/// receive. The commitment in each subshare is the same, and should be broadcast +/// to all subshare recipients. The secret subshare itself should be sent via +/// a private authenticated channel to the specific recipient which maps to it. +pub fn reshare_step_1( + share_i: &SigningShare, + rng: &mut R, + new_threshold: u16, + new_idents: &[Identifier], +) -> Result, Error> { + frost::keys::resharing::reshare_step_1(share_i, rng, new_threshold, new_idents) +} + +/// Verify and combine a set of secret subshares into a new FROST signing share. +/// +/// `our_ident` is the identifier for ourself. +/// +/// `old_pubkeys` is the old public key package for the group's joint FROST key. +/// +/// `new_threshold` is the desired new minimum signer threshold after resharing. +/// All signers participating in resharing must specify the same `new_threshold`. +/// +/// `new_idents` is the list of identifiers for peers to whom the secret subshares +/// are being distributed. Depending on use-case, these identifiers may be completely +/// new, or they may be the same as the old signing group from before resharing. +/// +/// `received_subshares` maps identifiers to the secret subshare sent by those peers. +/// We assume the commitment in each subshare is consistent with a commitment publicly +/// broadcasted by the sender, i.e. we assume each peer has not equivocated by sending +/// inconsistent commitments to different subshare recipients. +/// +/// The output is a new FROST secret signing share and public key package. The joint +/// public key will match the old joint public key, but the signing and verification +/// shares will be changed and will no longer be compatible with old shares from +/// before the resharing occurred. +/// +/// The caller MUST ensure at least `new_threshold` signers ACK the resharing as successful. +/// We recommend having each signer broadcast their public verification shares to confirm +/// the new set of shares are all consistent. Only then can the previous shares be safely +/// overwritten. +pub fn reshare_step_2( + our_ident: Identifier, + old_pubkeys: &PublicKeyPackage, + new_threshold: u16, + new_idents: &[Identifier], + received_subshares: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::resharing::reshare_step_2( + our_ident, + old_pubkeys, + new_threshold, + new_idents, + received_subshares, + ) +} diff --git a/frost-secp256k1/tests/integration_tests.rs b/frost-secp256k1/tests/integration_tests.rs index 58ba3e08..b3777d10 100644 --- a/frost-secp256k1/tests/integration_tests.rs +++ b/frost-secp256k1/tests/integration_tests.rs @@ -64,6 +64,15 @@ fn check_rts() { frost_core::tests::repairable::check_rts::(rng); } +#[test] +fn check_vsr() { + // let rng = thread_rng(); + use rand::SeedableRng; + let rng = rand::rngs::StdRng::seed_from_u64(0); + + frost_core::tests::resharing::check_vsr::(rng); +} + #[test] fn check_sign_with_dealer() { let rng = thread_rng(); diff --git a/gencode/src/main.rs b/gencode/src/main.rs index 0806901e..e3f85c61 100644 --- a/gencode/src/main.rs +++ b/gencode/src/main.rs @@ -24,6 +24,7 @@ use std::{ env, fs, io::Write, iter::zip, + path::Path, process::{Command, ExitCode, Stdio}, }; @@ -53,9 +54,12 @@ use regex::Regex; /// # Returns /// /// A list with data for each item, see above. -fn read_docs(filename: &str, suite_strings: &[&str]) -> Vec<(String, String, usize, usize)> { +fn read_docs( + filename: impl AsRef, + suite_strings: &[&str], +) -> Vec<(String, String, usize, usize)> { let mut docs = Vec::new(); - let code = fs::read_to_string(filename).unwrap(); + let code = fs::read_to_string(&filename).unwrap(); let re = Regex::new(concat!( // Enable multi-line (makes "^" match start of line) r"(?m)", @@ -106,12 +110,12 @@ fn read_docs(filename: &str, suite_strings: &[&str]) -> Vec<(String, String, usi /// for each reference in `original_suite_strings`. fn write_docs( docs: &[(String, String, usize, usize)], - filename: &str, + filename: impl AsRef, original_suite_strings: &[&str], new_suite_strings: &[&str], ) -> u8 { - let old_docs = read_docs(filename, new_suite_strings); - let mut code = fs::read_to_string(filename).unwrap(); + let old_docs = read_docs(&filename, new_suite_strings); + let mut code = fs::read_to_string(&filename).unwrap(); let original_code = code.clone(); // Map documentations by their identifiers @@ -192,15 +196,17 @@ fn main() -> ExitCode { let mut replaced = 0; let check = args.len() == 2 && args[1] == "--check"; - // Copy the frost-core repairable docs into ristretto255. + // Copy the frost-core repairable and resharing docs into ristretto255. // This will then be copied later down into the other ciphersuites. - let repairable_docs = read_docs("frost-core/src/keys/repairable.rs", &[]); - replaced |= write_docs( - &repairable_docs, - "frost-ristretto255/src/keys/repairable.rs", - &[], - &[], - ); + for module_name in ["repairable", "resharing"] { + let docs = read_docs(format!("frost-core/src/keys/{}.rs", module_name), &[]); + replaced |= write_docs( + &docs, + format!("frost-ristretto255/src/keys/{}.rs", module_name), + &[], + &[], + ); + } // Generate code or copy docs for other ciphersuites, using // ristretto255 as the canonical base. @@ -322,6 +328,7 @@ fn main() -> ExitCode { "dkg.md", "src/keys/dkg.rs", "src/keys/repairable.rs", + "src/keys/resharing.rs", "src/tests/batch.rs", "src/tests/coefficient_commitment.rs", "src/tests/proptests.rs",