Skip to main content

chains_sdk/threshold/frost/
keygen.rs

1//! FROST key generation using Shamir secret sharing.
2//!
3//! Implements trusted dealer key generation per RFC 9591 Appendix C.
4//! Splits a group signing key into shares using polynomial evaluation,
5//! with verifiable secret sharing (VSS) commitments.
6
7use crate::error::SignerError;
8use k256::elliptic_curve::ops::Reduce;
9use k256::{AffinePoint, ProjectivePoint, Scalar};
10use zeroize::Zeroizing;
11
12/// A participant's key package containing their secret share and group info.
13#[derive(Clone)]
14pub struct KeyPackage {
15    /// Participant identifier (1-based, non-zero).
16    pub identifier: u16,
17    /// The participant's secret signing share `sk_i = f(i)`.
18    pub(crate) secret_share: Zeroizing<Scalar>,
19    /// The group's combined public key `PK = G * s`.
20    pub group_public_key: AffinePoint,
21    /// Min participants required to sign (threshold).
22    pub min_participants: u16,
23    /// Max participants that hold shares.
24    pub max_participants: u16,
25}
26
27impl Drop for KeyPackage {
28    fn drop(&mut self) {
29        // secret_share is Zeroizing, automatically zeroed
30    }
31}
32
33impl KeyPackage {
34    /// Get the secret share scalar.
35    pub fn secret_share(&self) -> &Scalar {
36        &self.secret_share
37    }
38
39    /// Compute the public key corresponding to this share: `PK_i = G * sk_i`.
40    pub fn public_key(&self) -> AffinePoint {
41        (ProjectivePoint::GENERATOR * *self.secret_share).to_affine()
42    }
43
44    /// Get the secret share bytes (for serialization).
45    pub fn secret_share_bytes(&self) -> Zeroizing<[u8; 32]> {
46        let mut bytes = Zeroizing::new([0u8; 32]);
47        bytes.copy_from_slice(&self.secret_share.to_bytes());
48        bytes
49    }
50}
51
52/// VSS (Verifiable Secret Sharing) commitments.
53///
54/// Each commitment `C_k = G * a_k` where `a_k` is the k-th polynomial coefficient.
55/// Participants can verify their shares without learning the secret.
56#[derive(Clone, Debug)]
57pub struct VssCommitments {
58    /// The commitments `[G*a_0, G*a_1, ..., G*a_{t-1}]`.
59    pub commitments: Vec<AffinePoint>,
60}
61
62impl VssCommitments {
63    /// Verify a participant's share against the VSS commitments.
64    ///
65    /// Checks: `G * share == C_0 + i*C_1 + i^2*C_2 + ...`
66    pub fn verify_share(&self, identifier: u16, share: &Scalar) -> bool {
67        if identifier == 0 || self.commitments.is_empty() {
68            return false;
69        }
70
71        let i_scalar = Scalar::from(u64::from(identifier));
72        let lhs = ProjectivePoint::GENERATOR * *share;
73
74        // Evaluate the polynomial of commitments at point i using Horner's method
75        let mut rhs = ProjectivePoint::IDENTITY;
76        for ck in self.commitments.iter().rev() {
77            rhs = rhs * i_scalar + ProjectivePoint::from(*ck);
78        }
79
80        lhs == rhs
81    }
82}
83
84/// Output of trusted dealer key generation.
85pub struct KeyGenOutput {
86    /// Key packages for each participant.
87    pub key_packages: Vec<KeyPackage>,
88    /// VSS commitments for share verification.
89    pub vss_commitments: VssCommitments,
90    /// The group public key.
91    pub group_public_key: AffinePoint,
92}
93
94/// Generate key shares using a trusted dealer (RFC 9591 Appendix C).
95///
96/// The dealer holds the group secret key `s` and splits it into `max_participants`
97/// shares using a degree `(min_participants - 1)` polynomial, such that any
98/// `min_participants` shares can reconstruct the secret.
99///
100/// # Arguments
101/// * `group_secret` - The group signing key `s` (32-byte scalar)
102/// * `min_participants` - Minimum signers required (threshold `t`)
103/// * `max_participants` - Total number of shares to generate (`n`)
104///
105/// # Returns
106/// `KeyGenOutput` containing key packages and VSS commitments.
107pub fn trusted_dealer_keygen(
108    group_secret: &[u8; 32],
109    min_participants: u16,
110    max_participants: u16,
111) -> Result<KeyGenOutput, SignerError> {
112    if min_participants < 2 {
113        return Err(SignerError::InvalidPrivateKey(
114            "min_participants must be >= 2".into(),
115        ));
116    }
117    if max_participants < min_participants {
118        return Err(SignerError::InvalidPrivateKey(
119            "max_participants must be >= min_participants".into(),
120        ));
121    }
122
123    let s = scalar_from_bytes(group_secret)?;
124    let group_public_key = (ProjectivePoint::GENERATOR * s).to_affine();
125
126    // Generate random coefficients a_1, ..., a_{t-1}
127    let mut coefficients = Vec::with_capacity(min_participants as usize);
128    coefficients.push(s); // a_0 = s (the secret)
129
130    for _ in 1..min_participants {
131        let coeff = random_scalar()?;
132        coefficients.push(coeff);
133    }
134
135    // VSS commitments: C_k = G * a_k
136    let vss_commitments = VssCommitments {
137        commitments: coefficients
138            .iter()
139            .map(|a| (ProjectivePoint::GENERATOR * *a).to_affine())
140            .collect(),
141    };
142
143    // Generate shares: sk_i = f(i) for i = 1..=n
144    let mut key_packages = Vec::with_capacity(max_participants as usize);
145    for i in 1..=max_participants {
146        let x = Scalar::from(u64::from(i));
147        let share = polynomial_evaluate(&x, &coefficients);
148
149        key_packages.push(KeyPackage {
150            identifier: i,
151            secret_share: Zeroizing::new(share),
152            group_public_key,
153            min_participants,
154            max_participants,
155        });
156    }
157
158    // Zeroize coefficients
159    for c in coefficients.iter_mut() {
160        *c = Scalar::ZERO;
161    }
162
163    Ok(KeyGenOutput {
164        key_packages,
165        vss_commitments,
166        group_public_key,
167    })
168}
169
170/// Evaluate a polynomial at point `x` using Horner's method.
171///
172/// `f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_{t-1}*x^{t-1}`
173pub fn polynomial_evaluate(x: &Scalar, coefficients: &[Scalar]) -> Scalar {
174    let mut result = Scalar::ZERO;
175    for c in coefficients.iter().rev() {
176        result = result * x + c;
177    }
178    result
179}
180
181/// Compute the Lagrange interpolation coefficient for participant `x_i`
182/// given the set of participant identifiers.
183///
184/// `L_i(0) = Π_{j≠i} (x_j / (x_j - x_i))`
185pub fn derive_interpolating_value(
186    x_i: &Scalar,
187    participant_identifiers: &[Scalar],
188) -> Result<Scalar, SignerError> {
189    let mut numerator = Scalar::ONE;
190    let mut denominator = Scalar::ONE;
191
192    for x_j in participant_identifiers {
193        if x_j == x_i {
194            continue;
195        }
196        numerator *= x_j;
197        denominator *= *x_j - x_i;
198    }
199
200    // denominator must not be zero
201    if denominator == Scalar::ZERO {
202        return Err(SignerError::InvalidPrivateKey(
203            "duplicate participant identifiers".into(),
204        ));
205    }
206
207    Ok(numerator * denominator.invert().unwrap_or(Scalar::ZERO))
208}
209
210/// Parse a scalar from 32 bytes.
211pub fn scalar_from_bytes(bytes: &[u8; 32]) -> Result<Scalar, SignerError> {
212    let wide = k256::U256::from_be_slice(bytes);
213    let scalar = <Scalar as Reduce<k256::U256>>::reduce(wide);
214    if scalar == Scalar::ZERO {
215        return Err(SignerError::InvalidPrivateKey("scalar is zero".into()));
216    }
217    Ok(scalar)
218}
219
220/// Generate a random non-zero scalar.
221pub fn random_scalar() -> Result<Scalar, SignerError> {
222    let mut bytes = [0u8; 32];
223    crate::security::secure_random(&mut bytes)?;
224    let wide = k256::U256::from_be_slice(&bytes);
225    let scalar = <Scalar as Reduce<k256::U256>>::reduce(wide);
226    if scalar == Scalar::ZERO {
227        // Extremely unlikely, retry
228        return random_scalar();
229    }
230    Ok(scalar)
231}
232
233#[cfg(test)]
234#[allow(clippy::unwrap_used, clippy::expect_used)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_polynomial_evaluate() {
240        // f(x) = 3 + 2x + x^2 → f(2) = 3 + 4 + 4 = 11
241        let coeffs = [Scalar::from(3u64), Scalar::from(2u64), Scalar::from(1u64)];
242        let result = polynomial_evaluate(&Scalar::from(2u64), &coeffs);
243        assert_eq!(result, Scalar::from(11u64));
244    }
245
246    #[test]
247    fn test_trusted_dealer_keygen_2_of_3() {
248        let secret = [0x42u8; 32];
249        let out = trusted_dealer_keygen(&secret, 2, 3).unwrap();
250        assert_eq!(out.key_packages.len(), 3);
251        assert_eq!(out.vss_commitments.commitments.len(), 2);
252
253        // Verify each share against VSS commitments
254        for pkg in &out.key_packages {
255            assert!(out
256                .vss_commitments
257                .verify_share(pkg.identifier, pkg.secret_share()));
258        }
259    }
260
261    #[test]
262    fn test_share_reconstruction_lagrange() {
263        let secret = [0x42u8; 32];
264        let out = trusted_dealer_keygen(&secret, 2, 3).unwrap();
265
266        // Use shares 1 and 3 to reconstruct the secret
267        let ids = [Scalar::from(1u64), Scalar::from(3u64)];
268        let shares = [
269            *out.key_packages[0].secret_share(),
270            *out.key_packages[2].secret_share(),
271        ];
272
273        let mut reconstructed = Scalar::ZERO;
274        for (i, share) in shares.iter().enumerate() {
275            let lambda = derive_interpolating_value(&ids[i], &ids).unwrap();
276            reconstructed += lambda * share;
277        }
278
279        let original = scalar_from_bytes(&[0x42u8; 32]).unwrap();
280        assert_eq!(reconstructed, original);
281    }
282
283    #[test]
284    fn test_invalid_params() {
285        let secret = [0x42u8; 32];
286        assert!(trusted_dealer_keygen(&secret, 1, 3).is_err()); // min < 2
287        assert!(trusted_dealer_keygen(&secret, 3, 2).is_err()); // max < min
288    }
289
290    #[test]
291    fn test_zero_secret_rejected() {
292        assert!(trusted_dealer_keygen(&[0u8; 32], 2, 3).is_err());
293    }
294
295    #[test]
296    fn test_all_shares_reconstruct_to_secret() {
297        // Use all 3 shares (instead of just 2) — should still recover same secret
298        let secret = [0x42u8; 32];
299        let out = trusted_dealer_keygen(&secret, 2, 3).unwrap();
300
301        let ids: Vec<Scalar> = (1u64..=3).map(Scalar::from).collect();
302        let mut reconstructed = Scalar::ZERO;
303        for (i, pkg) in out.key_packages.iter().enumerate() {
304            let lambda = derive_interpolating_value(&ids[i], &ids).unwrap();
305            reconstructed += lambda * *pkg.secret_share();
306        }
307        let original = scalar_from_bytes(&[0x42u8; 32]).unwrap();
308        assert_eq!(reconstructed, original);
309    }
310
311    #[test]
312    fn test_public_key_share() {
313        let secret = [0x42u8; 32];
314        let out = trusted_dealer_keygen(&secret, 2, 3).unwrap();
315        for pkg in &out.key_packages {
316            let pk = pkg.public_key();
317            // G * sk_i should not be identity
318            assert_ne!(pk, AffinePoint::IDENTITY);
319        }
320    }
321
322    #[test]
323    fn test_keygen_3_of_5() {
324        let secret = [0x11u8; 32];
325        let out = trusted_dealer_keygen(&secret, 3, 5).unwrap();
326        assert_eq!(out.key_packages.len(), 5);
327        assert_eq!(out.vss_commitments.commitments.len(), 3); // degree t-1 = 2, plus constant
328        for pkg in &out.key_packages {
329            assert!(out
330                .vss_commitments
331                .verify_share(pkg.identifier, pkg.secret_share()));
332        }
333    }
334
335    #[test]
336    fn test_vss_verify_share_zero_identifier_rejected() {
337        let secret = [0x42u8; 32];
338        let out = trusted_dealer_keygen(&secret, 2, 3).unwrap();
339        // identifier = 0 should fail
340        assert!(!out
341            .vss_commitments
342            .verify_share(0, out.key_packages[0].secret_share()));
343    }
344}