mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
489
crypto/frost/src/key_gen.rs
Normal file
489
crypto/frost/src/key_gen.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use core::{convert::TryFrom, cmp::min, fmt};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::{Field, PrimeField};
|
||||
use group::Group;
|
||||
|
||||
use crate::{Curve, MultisigParams, MultisigKeys, FrostError};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn challenge<C: Curve>(l: usize, context: &str, R: &[u8], Am: &[u8]) -> C::F {
|
||||
let mut c = Vec::with_capacity(8 + context.len() + R.len() + Am.len());
|
||||
c.extend(&u64::try_from(l).unwrap().to_le_bytes());
|
||||
c.extend(context.as_bytes());
|
||||
c.extend(R); // R
|
||||
c.extend(Am); // A of the first commitment, which is what we're proving we have the private key
|
||||
// for
|
||||
// m of the rest of the commitments, authenticating them
|
||||
C::hash_to_F(&c)
|
||||
}
|
||||
|
||||
// Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and
|
||||
// the serialized commitments to be broadcasted over an authenticated channel to all parties
|
||||
// TODO: This potentially could return a much more robust serialized message, including a signature
|
||||
// of its entirety. The issue is it can't use its own key as it has no chain of custody behind it.
|
||||
// While we could ask for a key to be passed in, explicitly declaring the needed for authenticated
|
||||
// communications in the API itself, systems will likely already provide a authenticated
|
||||
// communication method making this redundant. It also doesn't guarantee the system which passed
|
||||
// the key is correctly using it, meaning we can only minimize risk so much
|
||||
// One notable improvement would be to include the index in the message. While the system must
|
||||
// still track this to determine if it's ready for the next step, and to remove duplicates, it
|
||||
// would ensure no counterparties presume the same index and this system didn't mislabel a
|
||||
// counterparty
|
||||
fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
context: &str,
|
||||
) -> (Vec<C::F>, Vec<C::G>, Vec<u8>) {
|
||||
let mut coefficients = Vec::with_capacity(params.t);
|
||||
let mut commitments = Vec::with_capacity(params.t);
|
||||
let mut serialized = Vec::with_capacity((C::G_len() * params.t) + C::G_len() + C::F_len());
|
||||
for j in 0 .. params.t {
|
||||
// Step 1: Generate t random values to form a polynomial with
|
||||
coefficients.push(C::F::random(&mut *rng));
|
||||
// Step 3: Generate public commitments
|
||||
commitments.push(C::generator_table() * coefficients[j]);
|
||||
// Serialize them for publication
|
||||
serialized.extend(&C::G_to_bytes(&commitments[j]));
|
||||
}
|
||||
|
||||
// Step 2: Provide a proof of knowledge
|
||||
// This can be deterministic as the PoK is a singleton never opened up to cooperative discussion
|
||||
// There's also no reason to spend the time and effort to make this deterministic besides a
|
||||
// general obsession with canonicity and determinism
|
||||
let k = C::F::random(rng);
|
||||
#[allow(non_snake_case)]
|
||||
let R = C::generator_table() * k;
|
||||
let c = challenge::<C>(params.i, context, &C::G_to_bytes(&R), &serialized);
|
||||
let s = k + (coefficients[0] * c);
|
||||
|
||||
serialized.extend(&C::G_to_bytes(&R));
|
||||
serialized.extend(&C::F_to_le_bytes(&s));
|
||||
|
||||
// Step 4: Broadcast
|
||||
(coefficients, commitments, serialized)
|
||||
}
|
||||
|
||||
// Verify the received data from the first round of key generation
|
||||
fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
context: &str,
|
||||
our_commitments: Vec<C::G>,
|
||||
serialized: &[Vec<u8>],
|
||||
) -> Result<Vec<Vec<C::G>>, FrostError> {
|
||||
// Deserialize all of the commitments, validating the input buffers as needed
|
||||
if serialized.len() != (params.n + 1) {
|
||||
Err(
|
||||
// Prevents a panic if serialized.len() == 0
|
||||
FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len()))
|
||||
)?;
|
||||
}
|
||||
|
||||
// Expect a null set of commitments for index 0 so the vector is guaranteed to line up with
|
||||
// actual indexes. Even if we did the offset internally, the system would need to write the vec
|
||||
// with the same offset in mind. Therefore, this trick which is probably slightly less efficient
|
||||
// yet keeps everything simple is preferred
|
||||
if serialized[0] != vec![] {
|
||||
Err(FrostError::NonEmptyParticipantZero)?;
|
||||
}
|
||||
|
||||
let commitments_len = params.t * C::G_len();
|
||||
let mut commitments = Vec::with_capacity(params.n + 1);
|
||||
commitments.push(vec![]);
|
||||
|
||||
let signature_len = C::G_len() + C::F_len();
|
||||
let mut first = true;
|
||||
let mut scalars = Vec::with_capacity((params.n - 1) * 3);
|
||||
let mut points = Vec::with_capacity((params.n - 1) * 3);
|
||||
for l in 1 ..= params.n {
|
||||
if l == params.i {
|
||||
if serialized[l].len() != 0 {
|
||||
Err(FrostError::DuplicatedIndex(l))?;
|
||||
}
|
||||
commitments.push(vec![]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if serialized[l].len() != (commitments_len + signature_len) {
|
||||
// Return an error with an approximation for how many commitments were included
|
||||
// Prevents errors if not even the signature was included
|
||||
if serialized[l].len() < signature_len {
|
||||
Err(FrostError::InvalidCommitmentQuantity(l, params.t, 0))?;
|
||||
}
|
||||
|
||||
Err(
|
||||
FrostError::InvalidCommitmentQuantity(
|
||||
l,
|
||||
params.t,
|
||||
// Could technically be x.y despite this returning x, yet any y is negligible
|
||||
// It could help with debugging to know a partial piece of data was read but this error
|
||||
// alone should be enough
|
||||
(serialized[l].len() - signature_len) / C::G_len()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
commitments.push(Vec::with_capacity(params.t));
|
||||
for o in 0 .. params.t {
|
||||
commitments[l].push(
|
||||
C::G_from_slice(
|
||||
&serialized[l][(o * C::G_len()) .. ((o + 1) * C::G_len())]
|
||||
).map_err(|_| FrostError::InvalidCommitment(l))?
|
||||
);
|
||||
}
|
||||
|
||||
// Step 5: Validate each proof of knowledge (prep)
|
||||
let mut u = C::F::one();
|
||||
if !first {
|
||||
u = C::F::random(&mut *rng);
|
||||
}
|
||||
|
||||
scalars.push(u);
|
||||
points.push(
|
||||
C::G_from_slice(
|
||||
&serialized[l][commitments_len .. commitments_len + C::G_len()]
|
||||
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?
|
||||
);
|
||||
|
||||
scalars.push(
|
||||
-C::F_from_le_slice(
|
||||
&serialized[l][commitments_len + C::G_len() .. serialized[l].len()]
|
||||
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))? * u
|
||||
);
|
||||
points.push(C::generator());
|
||||
|
||||
let c = challenge::<C>(
|
||||
l,
|
||||
context,
|
||||
&serialized[l][commitments_len .. commitments_len + C::G_len()],
|
||||
&serialized[l][0 .. commitments_len]
|
||||
);
|
||||
|
||||
if first {
|
||||
scalars.push(c);
|
||||
first = false;
|
||||
} else {
|
||||
scalars.push(c * u);
|
||||
}
|
||||
points.push(commitments[l][0]);
|
||||
}
|
||||
|
||||
// Step 5: Implementation
|
||||
// Uses batch verification to optimize the success case dramatically
|
||||
// On failure, the cost is now this + blame, yet that should happen infrequently
|
||||
if C::multiexp_vartime(&scalars, &points) != C::G::identity() {
|
||||
for l in 1 ..= params.n {
|
||||
if l == params.i {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let R = C::G_from_slice(
|
||||
&serialized[l][commitments_len .. commitments_len + C::G_len()]
|
||||
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
|
||||
|
||||
let s = C::F_from_le_slice(
|
||||
&serialized[l][commitments_len + C::G_len() .. serialized[l].len()]
|
||||
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
|
||||
|
||||
let c = challenge::<C>(
|
||||
l,
|
||||
context,
|
||||
&serialized[l][commitments_len .. commitments_len + C::G_len()],
|
||||
&serialized[l][0 .. commitments_len]
|
||||
);
|
||||
|
||||
if R != ((C::generator_table() * s) + (commitments[l][0] * (C::F::zero() - &c))) {
|
||||
Err(FrostError::InvalidProofOfKnowledge(l))?;
|
||||
}
|
||||
}
|
||||
|
||||
Err(FrostError::InternalError("batch validation is broken".to_string()))?;
|
||||
}
|
||||
|
||||
// Write in our own commitments
|
||||
commitments[params.i] = our_commitments;
|
||||
|
||||
Ok(commitments)
|
||||
}
|
||||
|
||||
fn polynomial<F: PrimeField>(
|
||||
coefficients: &[F],
|
||||
i: usize
|
||||
) -> F {
|
||||
let i = F::from(u64::try_from(i).unwrap());
|
||||
let mut share = F::zero();
|
||||
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
|
||||
share += coefficient;
|
||||
if idx != (coefficients.len() - 1) {
|
||||
share *= i;
|
||||
}
|
||||
}
|
||||
share
|
||||
}
|
||||
|
||||
// Implements round 1, step 5 and round 2, step 1 of FROST key generation
|
||||
// Returns our secret share part, commitments for the next step, and a vector for each
|
||||
// counterparty to receive
|
||||
fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
context: &str,
|
||||
coefficients: Vec<C::F>,
|
||||
our_commitments: Vec<C::G>,
|
||||
commitments: &[Vec<u8>],
|
||||
) -> Result<(C::F, Vec<Vec<C::G>>, Vec<Vec<u8>>), FrostError> {
|
||||
let commitments = verify_r1::<R, C>(rng, params, context, our_commitments, commitments)?;
|
||||
|
||||
// Step 1: Generate secret shares for all other parties
|
||||
let mut res = Vec::with_capacity(params.n + 1);
|
||||
res.push(vec![]);
|
||||
for i in 1 ..= params.n {
|
||||
// Don't push our own to the byte buffer which is meant to be sent around
|
||||
// An app developer could accidentally send it. Best to keep this black boxed
|
||||
if i == params.i {
|
||||
res.push(vec![]);
|
||||
continue
|
||||
}
|
||||
|
||||
res.push(C::F_to_le_bytes(&polynomial(&coefficients, i)));
|
||||
}
|
||||
|
||||
// Calculate our own share
|
||||
let share = polynomial(&coefficients, params.i);
|
||||
|
||||
// The secret shares are discarded here, not cleared. While any system which leaves its memory
|
||||
// accessible is likely totally lost already, making the distinction meaningless when the key gen
|
||||
// system acts as the signer system and therefore actively holds the signing key anyways, it
|
||||
// should be overwritten with /dev/urandom in the name of security (which still doesn't meet
|
||||
// requirements for secure data deletion yet those requirements expect hardware access which is
|
||||
// far past what this library can reasonably counter)
|
||||
// TODO: Zero out the coefficients
|
||||
|
||||
Ok((share, commitments, res))
|
||||
}
|
||||
|
||||
/// Finishes round 2 and returns both the secret share and the serialized public key.
|
||||
/// This key is not usable until all parties confirm they have completed the protocol without
|
||||
/// issue, yet simply confirming protocol completion without issue is enough to confirm the same
|
||||
/// key was generated as long as a lack of duplicated commitments was also confirmed when they were
|
||||
/// broadcasted initially
|
||||
fn complete_r2<C: Curve>(
|
||||
params: MultisigParams,
|
||||
share: C::F,
|
||||
commitments: &[Vec<C::G>],
|
||||
// Vec to preserve ownership
|
||||
serialized: Vec<Vec<u8>>,
|
||||
) -> Result<MultisigKeys<C>, FrostError> {
|
||||
// Step 2. Verify each share
|
||||
if serialized.len() != (params.n + 1) {
|
||||
Err(
|
||||
FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len()))
|
||||
)?;
|
||||
}
|
||||
|
||||
if (commitments[0].len() != 0) || (serialized[0].len() != 0) {
|
||||
Err(FrostError::NonEmptyParticipantZero)?;
|
||||
}
|
||||
|
||||
// Deserialize them
|
||||
let mut shares: Vec<C::F> = vec![C::F::zero()];
|
||||
for i in 1 .. serialized.len() {
|
||||
if i == params.i {
|
||||
if serialized[i].len() != 0 {
|
||||
Err(FrostError::DuplicatedIndex(i))?;
|
||||
}
|
||||
shares.push(C::F::zero());
|
||||
continue;
|
||||
}
|
||||
shares.push(C::F_from_le_slice(&serialized[i]).map_err(|_| FrostError::InvalidShare(i))?);
|
||||
}
|
||||
|
||||
|
||||
for l in 1 ..= params.n {
|
||||
if l == params.i {
|
||||
continue;
|
||||
}
|
||||
|
||||
let i_scalar = C::F::from(u64::try_from(params.i).unwrap());
|
||||
let mut exp = C::F::one();
|
||||
let mut exps = Vec::with_capacity(params.t);
|
||||
for _ in 0 .. params.t {
|
||||
exps.push(exp);
|
||||
exp *= i_scalar;
|
||||
}
|
||||
|
||||
// Doesn't use multiexp_vartime with -shares[l] due to not being able to push to commitments
|
||||
if C::multiexp_vartime(&exps, &commitments[l]) != (C::generator_table() * shares[l]) {
|
||||
Err(FrostError::InvalidCommitment(l))?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Clear the original share
|
||||
|
||||
let mut secret_share = share;
|
||||
for remote_share in shares {
|
||||
secret_share += remote_share;
|
||||
}
|
||||
|
||||
let mut verification_shares = vec![C::G::identity()];
|
||||
for i in 1 ..= params.n {
|
||||
let mut exps = vec![];
|
||||
let mut cs = vec![];
|
||||
for j in 1 ..= params.n {
|
||||
for k in 0 .. params.t {
|
||||
let mut exp = C::F::one();
|
||||
for _ in 0 .. k {
|
||||
exp *= C::F::from(u64::try_from(i).unwrap());
|
||||
}
|
||||
exps.push(exp);
|
||||
cs.push(commitments[j][k]);
|
||||
}
|
||||
}
|
||||
verification_shares.push(C::multiexp_vartime(&exps, &cs));
|
||||
}
|
||||
|
||||
debug_assert_eq!(
|
||||
C::generator_table() * secret_share,
|
||||
verification_shares[params.i]
|
||||
);
|
||||
|
||||
let mut group_key = C::G::identity();
|
||||
for j in 1 ..= params.n {
|
||||
group_key += commitments[j][0];
|
||||
}
|
||||
|
||||
// TODO: Clear serialized and shares
|
||||
|
||||
Ok(MultisigKeys { params, secret_share, group_key, verification_shares, offset: None } )
|
||||
}
|
||||
|
||||
/// State of a Key Generation machine
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum State {
|
||||
Fresh,
|
||||
GeneratedCoefficients,
|
||||
GeneratedSecretShares,
|
||||
Complete,
|
||||
}
|
||||
|
||||
impl fmt::Display for State {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// State machine which manages key generation
|
||||
#[allow(non_snake_case)]
|
||||
pub struct StateMachine<C: Curve> {
|
||||
params: MultisigParams,
|
||||
context: String,
|
||||
state: State,
|
||||
coefficients: Option<Vec<C::F>>,
|
||||
our_commitments: Option<Vec<C::G>>,
|
||||
secret: Option<C::F>,
|
||||
commitments: Option<Vec<Vec<C::G>>>
|
||||
}
|
||||
|
||||
impl<C: Curve> StateMachine<C> {
|
||||
/// Creates a new machine to generate a key for the specified curve in the specified multisig
|
||||
// The context string must be unique among multisigs
|
||||
pub fn new(params: MultisigParams, context: String) -> StateMachine<C> {
|
||||
StateMachine {
|
||||
params,
|
||||
context,
|
||||
state: State::Fresh,
|
||||
coefficients: None,
|
||||
our_commitments: None,
|
||||
secret: None,
|
||||
commitments: None
|
||||
}
|
||||
}
|
||||
|
||||
/// Start generating a key according to the FROST DKG spec
|
||||
/// Returns a serialized list of commitments to be sent to all parties over an authenticated
|
||||
/// channel. If any party submits multiple sets of commitments, they MUST be treated as malicious
|
||||
pub fn generate_coefficients<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state != State::Fresh {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::Fresh, self.state))?;
|
||||
}
|
||||
|
||||
let (coefficients, commitments, serialized) = generate_key_r1::<R, C>(
|
||||
rng,
|
||||
&self.params,
|
||||
&self.context,
|
||||
);
|
||||
|
||||
self.coefficients = Some(coefficients);
|
||||
self.our_commitments = Some(commitments);
|
||||
self.state = State::GeneratedCoefficients;
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
/// Continue generating a key
|
||||
/// Takes in everyone else's commitments, which are expected to be in a Vec where participant
|
||||
/// index = Vec index. An empty vector is expected at index 0 to allow for this. An empty vector
|
||||
/// is also expected at index i which is locally handled. Returns a byte vector representing a
|
||||
/// secret share for each other participant which should be encrypted before sending
|
||||
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
commitments: Vec<Vec<u8>>,
|
||||
) -> Result<Vec<Vec<u8>>, FrostError> {
|
||||
if self.state != State::GeneratedCoefficients {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::GeneratedCoefficients, self.state))?;
|
||||
}
|
||||
|
||||
let (secret, commitments, shares) = generate_key_r2::<R, C>(
|
||||
rng,
|
||||
&self.params,
|
||||
&self.context,
|
||||
self.coefficients.take().unwrap(),
|
||||
self.our_commitments.take().unwrap(),
|
||||
&commitments,
|
||||
)?;
|
||||
|
||||
self.secret = Some(secret);
|
||||
self.commitments = Some(commitments);
|
||||
self.state = State::GeneratedSecretShares;
|
||||
Ok(shares)
|
||||
}
|
||||
|
||||
/// Complete key generation
|
||||
/// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index =
|
||||
/// Vec index with an empty vector at index 0 and index i. Returns a byte vector representing the
|
||||
/// group's public key, while setting a valid secret share inside the machine. > t participants
|
||||
/// must report completion without issue before this key can be considered usable, yet you should
|
||||
/// wait for all participants to report as such
|
||||
pub fn complete(
|
||||
&mut self,
|
||||
shares: Vec<Vec<u8>>,
|
||||
) -> Result<MultisigKeys<C>, FrostError> {
|
||||
if self.state != State::GeneratedSecretShares {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::GeneratedSecretShares, self.state))?;
|
||||
}
|
||||
|
||||
let keys = complete_r2(
|
||||
self.params,
|
||||
self.secret.take().unwrap(),
|
||||
&self.commitments.take().unwrap(),
|
||||
shares,
|
||||
)?;
|
||||
|
||||
self.state = State::Complete;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub fn params(&self) -> MultisigParams {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
pub fn state(&self) -> State {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user