Skip to main content

chains_sdk/threshold/frost/
signing.rs

1//! FROST two-round signing protocol (RFC 9591 Section 5).
2//!
3//! Implements the FROST(secp256k1, SHA-256) ciphersuite.
4//! contextString = "FROST-secp256k1-SHA256-v1"
5
6use crate::error::SignerError;
7use crate::threshold::frost::keygen::{derive_interpolating_value, KeyPackage};
8#[allow(unused_imports)]
9use k256::elliptic_curve::group::GroupEncoding;
10use k256::elliptic_curve::ops::Reduce;
11use k256::elliptic_curve::sec1::ToEncodedPoint;
12use k256::{AffinePoint, ProjectivePoint, Scalar};
13use sha2::{Digest, Sha256};
14use zeroize::Zeroizing;
15
16/// Context string for FROST(secp256k1, SHA-256).
17const CONTEXT_STRING: &[u8] = b"FROST-secp256k1-SHA256-v1";
18
19// ─── Data Types ──────────────────────────────────────────────────────
20
21/// Secret nonces generated in round 1 (MUST be used exactly once, then discarded).
22pub struct SigningNonces {
23    /// Hiding nonce `d_i`.
24    pub(crate) hiding: Zeroizing<Scalar>,
25    /// Binding nonce `e_i`.
26    pub(crate) binding: Zeroizing<Scalar>,
27    /// The corresponding public commitments.
28    pub commitments: SigningCommitments,
29}
30
31impl Drop for SigningNonces {
32    fn drop(&mut self) {
33        // hiding and binding are Zeroizing<Scalar>
34    }
35}
36
37/// Public commitments broadcast in round 1.
38#[derive(Clone, Debug)]
39pub struct SigningCommitments {
40    /// Participant identifier.
41    pub identifier: u16,
42    /// Hiding nonce commitment `D_i = G * d_i`.
43    pub hiding: AffinePoint,
44    /// Binding nonce commitment `E_i = G * e_i`.
45    pub binding: AffinePoint,
46}
47
48/// A partial signature share produced in round 2.
49#[derive(Clone)]
50pub struct SignatureShare {
51    /// Participant identifier.
52    pub identifier: u16,
53    /// The partial signature scalar `z_i`.
54    pub share: Scalar,
55}
56
57impl core::fmt::Debug for SignatureShare {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        f.debug_struct("SignatureShare")
60            .field("identifier", &self.identifier)
61            .field("share", &"[REDACTED]")
62            .finish()
63    }
64}
65
66/// A FROST Schnorr signature (compressed point R || scalar s).
67#[derive(Clone, Debug)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69pub struct FrostSignature {
70    /// The group commitment point R (33 bytes, SEC1 compressed).
71    pub r_bytes: Vec<u8>,
72    /// The signature scalar s (32 bytes).
73    pub s_bytes: [u8; 32],
74}
75
76impl FrostSignature {
77    /// Encode as raw bytes: `R (33 bytes) || s (32 bytes)`.
78    pub fn to_bytes(&self) -> Vec<u8> {
79        let mut out = Vec::with_capacity(65);
80        out.extend_from_slice(&self.r_bytes);
81        out.extend_from_slice(&self.s_bytes);
82        out
83    }
84}
85
86// ─── Domain-Separated Hash Functions (RFC 9591 Section 6.5) ──────────
87
88/// H1: Hash to scalar for nonce derivation.
89fn h1(data: &[u8]) -> Scalar {
90    hash_to_scalar(b"rho", data)
91}
92
93/// H2: Hash to scalar for challenge computation.
94fn h2(data: &[u8]) -> Scalar {
95    hash_to_scalar(b"chal", data)
96}
97
98/// H3: Hash to scalar for nonce generation (RFC 9591 spec completeness).
99#[allow(dead_code)]
100fn h3(data: &[u8]) -> Scalar {
101    hash_to_scalar(b"nonce", data)
102}
103
104/// H4: Hash for message processing.
105fn h4(data: &[u8]) -> [u8; 32] {
106    hash_to_bytes(b"msg", data)
107}
108
109/// H5: Hash for commitment processing.
110fn h5(data: &[u8]) -> [u8; 32] {
111    hash_to_bytes(b"com", data)
112}
113
114/// Hash to scalar using expand_message_xmd (RFC 9380 Section 5.2).
115///
116/// Uses SHA-256 with domain separation: `contextString || tag`.
117fn hash_to_scalar(tag: &[u8], data: &[u8]) -> Scalar {
118    // Build DST: contextString || tag
119    let mut dst = Vec::with_capacity(CONTEXT_STRING.len() + tag.len());
120    dst.extend_from_slice(CONTEXT_STRING);
121    dst.extend_from_slice(tag);
122
123    // expand_message_xmd with desired output length = 48 bytes (for wide reduction)
124    let expanded = expand_message_xmd(data, &dst, 48);
125
126    // Reduce 48 bytes to a scalar (mod group order)
127    let mut wide = [0u8; 48];
128    wide.copy_from_slice(&expanded);
129    scalar_from_wide(&wide)
130}
131
132/// Hash to bytes using domain separation.
133fn hash_to_bytes(tag: &[u8], data: &[u8]) -> [u8; 32] {
134    let mut h = Sha256::new();
135    h.update(CONTEXT_STRING);
136    h.update(tag);
137    h.update(data);
138    let result = h.finalize();
139    let mut out = [0u8; 32];
140    out.copy_from_slice(&result);
141    out
142}
143
144/// expand_message_xmd (RFC 9380 Section 5.2) using SHA-256.
145fn expand_message_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec<u8> {
146    let b_in_bytes = 32usize; // SHA-256 output
147    let ell = len_in_bytes.div_ceil(b_in_bytes);
148
149    // DST_prime = DST || I2OSP(len(DST), 1)
150    let dst_prime_len = dst.len() as u8;
151
152    // Z_pad = I2OSP(0, b_in_bytes) = 64 zero bytes for SHA-256 block size
153    let z_pad = [0u8; 64];
154
155    // l_i_b_str = I2OSP(len_in_bytes, 2)
156    let l_i_b = (len_in_bytes as u16).to_be_bytes();
157
158    // b_0 = H(Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime)
159    let mut h0 = Sha256::new();
160    h0.update(z_pad);
161    h0.update(msg);
162    h0.update(l_i_b);
163    h0.update([0u8]);
164    h0.update(dst);
165    h0.update([dst_prime_len]);
166    let b_0 = h0.finalize();
167
168    // b_1 = H(b_0 || I2OSP(1, 1) || DST_prime)
169    let mut h1 = Sha256::new();
170    h1.update(b_0);
171    h1.update([1u8]);
172    h1.update(dst);
173    h1.update([dst_prime_len]);
174    let mut b_vals = vec![h1.finalize()];
175
176    for i in 2..=ell {
177        // b_i = H(strxor(b_0, b_{i-1}) || I2OSP(i, 1) || DST_prime)
178        let prev = &b_vals[i - 2];
179        let mut xored = [0u8; 32];
180        for (j, byte) in xored.iter_mut().enumerate() {
181            *byte = b_0[j] ^ prev[j];
182        }
183        let mut hi = Sha256::new();
184        hi.update(xored);
185        hi.update([i as u8]);
186        hi.update(dst);
187        hi.update([dst_prime_len]);
188        b_vals.push(hi.finalize());
189    }
190
191    let mut output = Vec::with_capacity(len_in_bytes);
192    for b in &b_vals {
193        output.extend_from_slice(b);
194    }
195    output.truncate(len_in_bytes);
196    output
197}
198
199/// Reduce 48 bytes to a scalar using wide reduction (mod group order).
200fn scalar_from_wide(bytes: &[u8; 48]) -> Scalar {
201    // Interpret as big-endian 384-bit integer, reduce mod n
202    // k256 doesn't directly support 384-bit, so we do it manually
203    let mut u256_hi = [0u8; 32];
204    let mut u256_lo = [0u8; 32];
205    u256_hi[16..].copy_from_slice(&bytes[..16]);
206    u256_lo.copy_from_slice(&bytes[16..]);
207
208    let hi = k256::U256::from_be_slice(&u256_hi);
209    let lo = k256::U256::from_be_slice(&u256_lo);
210
211    // result = hi * 2^256 + lo (mod n)
212    let hi_scalar = <Scalar as Reduce<k256::U256>>::reduce(hi);
213    let lo_scalar = <Scalar as Reduce<k256::U256>>::reduce(lo);
214
215    // 2^256 mod n
216    let two_256_mod_n = {
217        let _bytes = [0u8; 32];
218        // 2^256 mod n = 2^256 - n = 0x14551231950B75FC4402DA1732FC9BEBF
219        // Actually, the easiest way: just reduce a U256 of all zeros from the top
220        let max = k256::U256::from_be_hex(
221            "0000000000000000000000000000000100000000000000000000000000000000",
222        );
223        <Scalar as Reduce<k256::U256>>::reduce(max)
224    };
225
226    hi_scalar * two_256_mod_n + lo_scalar
227}
228
229// ─── Round 1: Commitment ────────────────────────────────────────────
230
231/// FROST Round 1: Generate nonces and commitments.
232///
233/// Each participant calls this once per signing session.
234/// The secret nonces MUST NOT be reused across sessions.
235pub fn commit(key_package: &KeyPackage) -> Result<SigningNonces, SignerError> {
236    let hiding = crate::threshold::frost::keygen::random_scalar()?;
237    let binding = crate::threshold::frost::keygen::random_scalar()?;
238
239    let hiding_commitment = (ProjectivePoint::GENERATOR * hiding).to_affine();
240    let binding_commitment = (ProjectivePoint::GENERATOR * binding).to_affine();
241
242    Ok(SigningNonces {
243        hiding: Zeroizing::new(hiding),
244        binding: Zeroizing::new(binding),
245        commitments: SigningCommitments {
246            identifier: key_package.identifier,
247            hiding: hiding_commitment,
248            binding: binding_commitment,
249        },
250    })
251}
252
253// ─── Binding Factor Computation ──────────────────────────────────────
254
255/// Compute the binding factor for a single participant.
256fn compute_binding_factor(
257    group_public_key: &AffinePoint,
258    commitments_list: &[SigningCommitments],
259    identifier: u16,
260    message: &[u8],
261) -> Scalar {
262    // encoded_commitments_hash = H5(encode(commitments))
263    let mut commit_data = Vec::new();
264    for c in commitments_list {
265        let hiding_enc = ProjectivePoint::from(c.hiding)
266            .to_affine()
267            .to_encoded_point(true);
268        let binding_enc = ProjectivePoint::from(c.binding)
269            .to_affine()
270            .to_encoded_point(true);
271        commit_data.extend_from_slice(hiding_enc.as_bytes());
272        commit_data.extend_from_slice(binding_enc.as_bytes());
273    }
274    let encoded_commitments_hash = h5(&commit_data);
275
276    // msg_hash = H4(message)
277    let msg_hash = h4(message);
278
279    // binding_factor_input = group_public_key || msg_hash || encoded_commitments_hash || I2OSP(identifier, 2)
280    let pk_enc = ProjectivePoint::from(*group_public_key)
281        .to_affine()
282        .to_encoded_point(true);
283    let mut input = Vec::new();
284    input.extend_from_slice(pk_enc.as_bytes());
285    input.extend_from_slice(&msg_hash);
286    input.extend_from_slice(&encoded_commitments_hash);
287
288    // Participant identifier as bytes — RFC says to fill remaining with zeros to Ns (32 bytes)
289    let mut id_bytes = [0u8; 32];
290    let id_be = (identifier as u64).to_be_bytes();
291    id_bytes[24..].copy_from_slice(&id_be);
292    input.extend_from_slice(&id_bytes);
293
294    h1(&input)
295}
296
297/// Compute the group commitment `R = Σ(D_i + ρ_i * E_i)`.
298fn compute_group_commitment(
299    commitments_list: &[SigningCommitments],
300    binding_factors: &[(u16, Scalar)],
301) -> ProjectivePoint {
302    let mut r = ProjectivePoint::IDENTITY;
303
304    for c in commitments_list {
305        let rho = binding_factors
306            .iter()
307            .find(|(id, _)| *id == c.identifier)
308            .map(|(_, bf)| *bf)
309            .unwrap_or(Scalar::ZERO);
310
311        r += ProjectivePoint::from(c.hiding) + ProjectivePoint::from(c.binding) * rho;
312    }
313
314    r
315}
316
317// ─── Round 2: Signature Share Generation ─────────────────────────────
318
319/// FROST Round 2: Generate a partial signature share.
320///
321/// Each participant produces `z_i = d_i + (e_i * ρ_i) + λ_i * s_i * c`
322/// where `c` is the challenge hash.
323pub fn sign(
324    key_package: &KeyPackage,
325    nonces: SigningNonces,
326    commitments_list: &[SigningCommitments],
327    message: &[u8],
328) -> Result<SignatureShare, SignerError> {
329    // Compute binding factors for all participants
330    let mut binding_factors = Vec::new();
331    for c in commitments_list {
332        let bf = compute_binding_factor(
333            &key_package.group_public_key,
334            commitments_list,
335            c.identifier,
336            message,
337        );
338        binding_factors.push((c.identifier, bf));
339    }
340
341    // Group commitment R
342    let group_commitment = compute_group_commitment(commitments_list, &binding_factors);
343    let r_enc = group_commitment.to_affine().to_encoded_point(true);
344
345    // Challenge: c = H2(R || PK || message)
346    let pk_enc = ProjectivePoint::from(key_package.group_public_key)
347        .to_affine()
348        .to_encoded_point(true);
349    let mut challenge_input = Vec::new();
350    challenge_input.extend_from_slice(r_enc.as_bytes());
351    challenge_input.extend_from_slice(pk_enc.as_bytes());
352    challenge_input.extend_from_slice(message);
353    let challenge = h2(&challenge_input);
354
355    // My binding factor
356    let my_rho = binding_factors
357        .iter()
358        .find(|(id, _)| *id == key_package.identifier)
359        .map(|(_, bf)| *bf)
360        .ok_or_else(|| SignerError::SigningFailed("participant not in commitments list".into()))?;
361
362    // Lagrange coefficient
363    let participant_ids: Vec<Scalar> = commitments_list
364        .iter()
365        .map(|c| Scalar::from(u64::from(c.identifier)))
366        .collect();
367    let lambda = derive_interpolating_value(
368        &Scalar::from(u64::from(key_package.identifier)),
369        &participant_ids,
370    )?;
371
372    // z_i = d_i + (e_i * ρ_i) + λ_i * s_i * c
373    let z = *nonces.hiding
374        + (*nonces.binding * my_rho)
375        + (lambda * *key_package.secret_share() * challenge);
376
377    Ok(SignatureShare {
378        identifier: key_package.identifier,
379        share: z,
380    })
381}
382
383// ─── Signature Aggregation ───────────────────────────────────────────
384
385/// Aggregate partial signature shares into a final FROST signature.
386///
387/// The coordinator collects all `SignatureShare` values and produces
388/// a standard Schnorr signature `(R, s)`.
389pub fn aggregate(
390    commitments_list: &[SigningCommitments],
391    sig_shares: &[SignatureShare],
392    group_public_key: &AffinePoint,
393    message: &[u8],
394) -> Result<FrostSignature, SignerError> {
395    if sig_shares.len() < 2 {
396        return Err(SignerError::SigningFailed(
397            "need at least 2 signature shares".into(),
398        ));
399    }
400
401    // Compute binding factors
402    let mut binding_factors = Vec::new();
403    for c in commitments_list {
404        let bf = compute_binding_factor(group_public_key, commitments_list, c.identifier, message);
405        binding_factors.push((c.identifier, bf));
406    }
407
408    // Group commitment R
409    let group_commitment = compute_group_commitment(commitments_list, &binding_factors);
410
411    // Aggregate: s = Σ z_i
412    let mut s = Scalar::ZERO;
413    for share in sig_shares {
414        s += share.share;
415    }
416
417    let r_enc = group_commitment.to_affine().to_encoded_point(true);
418
419    Ok(FrostSignature {
420        r_bytes: r_enc.as_bytes().to_vec(),
421        s_bytes: s.to_bytes().into(),
422    })
423}
424
425/// Verify a FROST signature against the group public key.
426///
427/// Standard Schnorr verification: `s * G == R + c * PK`
428pub fn verify(
429    signature: &FrostSignature,
430    group_public_key: &AffinePoint,
431    message: &[u8],
432) -> Result<bool, SignerError> {
433    // Parse R
434    let r_ct = AffinePoint::from_bytes(signature.r_bytes.as_slice().into());
435    if !bool::from(r_ct.is_some()) {
436        return Ok(false);
437    }
438    // Safe: is_some() verified above. CtOption::unwrap() is constant-time.
439    #[allow(clippy::unwrap_used)]
440    let r_point = ProjectivePoint::from(r_ct.unwrap());
441
442    // Parse s
443    let s_wide = k256::U256::from_be_slice(&signature.s_bytes);
444    let s_scalar = <Scalar as Reduce<k256::U256>>::reduce(s_wide);
445
446    // Challenge: c = H2(R || PK || message)
447    let r_enc = r_point.to_affine().to_encoded_point(true);
448    let pk_enc = ProjectivePoint::from(*group_public_key)
449        .to_affine()
450        .to_encoded_point(true);
451    let mut challenge_input = Vec::new();
452    challenge_input.extend_from_slice(r_enc.as_bytes());
453    challenge_input.extend_from_slice(pk_enc.as_bytes());
454    challenge_input.extend_from_slice(message);
455    let challenge = h2(&challenge_input);
456
457    // Verify: s * G == R + c * PK
458    let lhs = ProjectivePoint::GENERATOR * s_scalar;
459    let rhs = r_point + ProjectivePoint::from(*group_public_key) * challenge;
460
461    Ok(lhs == rhs)
462}
463
464/// Verify a single participant's signature share (identifiable abort).
465///
466/// Checks: `z_i * G == D_i + ρ_i * E_i + λ_i * c * PK_i`
467pub fn verify_share(
468    share: &SignatureShare,
469    commitment: &SigningCommitments,
470    public_key_share: &AffinePoint,
471    group_public_key: &AffinePoint,
472    commitments_list: &[SigningCommitments],
473    message: &[u8],
474) -> Result<bool, SignerError> {
475    // Binding factor
476    let binding_factor = compute_binding_factor(
477        group_public_key,
478        commitments_list,
479        share.identifier,
480        message,
481    );
482
483    // Binding factors for group commitment
484    let mut binding_factors = Vec::new();
485    for c in commitments_list {
486        let bf = compute_binding_factor(group_public_key, commitments_list, c.identifier, message);
487        binding_factors.push((c.identifier, bf));
488    }
489
490    // Group commitment R
491    let group_commitment = compute_group_commitment(commitments_list, &binding_factors);
492    let r_enc = group_commitment.to_affine().to_encoded_point(true);
493
494    // Challenge
495    let pk_enc = ProjectivePoint::from(*group_public_key)
496        .to_affine()
497        .to_encoded_point(true);
498    let mut challenge_input = Vec::new();
499    challenge_input.extend_from_slice(r_enc.as_bytes());
500    challenge_input.extend_from_slice(pk_enc.as_bytes());
501    challenge_input.extend_from_slice(message);
502    let challenge = h2(&challenge_input);
503
504    // Lagrange coefficient
505    let participant_ids: Vec<Scalar> = commitments_list
506        .iter()
507        .map(|c| Scalar::from(u64::from(c.identifier)))
508        .collect();
509    let lambda =
510        derive_interpolating_value(&Scalar::from(u64::from(share.identifier)), &participant_ids)?;
511
512    // lhs = z_i * G
513    let lhs = ProjectivePoint::GENERATOR * share.share;
514
515    // rhs = D_i + ρ_i * E_i + λ_i * c * PK_i
516    let rhs = ProjectivePoint::from(commitment.hiding)
517        + ProjectivePoint::from(commitment.binding) * binding_factor
518        + ProjectivePoint::from(*public_key_share) * (lambda * challenge);
519
520    Ok(lhs == rhs)
521}
522
523/// Identify misbehaving participants by verifying each signature share.
524///
525/// Returns a list of participant identifiers whose shares are invalid.
526/// If the list is empty, all shares are valid.
527///
528/// This is used for **identifiable abort**: if signature aggregation fails
529/// verification, the coordinator can pinpoint exactly which participant(s)
530/// submitted bad shares.
531///
532/// # Arguments
533/// - `sig_shares` — All collected signature shares
534/// - `commitments_list` — All commitments from round 1
535/// - `key_packages` — Key packages (needed for per-share public keys)
536/// - `group_public_key` — The group public key
537/// - `message` — The signed message
538pub fn identify_misbehaving(
539    sig_shares: &[SignatureShare],
540    commitments_list: &[SigningCommitments],
541    key_packages: &[KeyPackage],
542    group_public_key: &AffinePoint,
543    message: &[u8],
544) -> Result<Vec<u16>, SignerError> {
545    let mut cheaters = Vec::new();
546
547    for share in sig_shares {
548        // Find the commitment for this participant
549        let commitment = commitments_list
550            .iter()
551            .find(|c| c.identifier == share.identifier);
552
553        let commitment = match commitment {
554            Some(c) => c,
555            None => {
556                cheaters.push(share.identifier);
557                continue;
558            }
559        };
560
561        // Find the key package for this participant
562        let key_package = key_packages
563            .iter()
564            .find(|kp| kp.identifier == share.identifier);
565
566        let key_package = match key_package {
567            Some(kp) => kp,
568            None => {
569                cheaters.push(share.identifier);
570                continue;
571            }
572        };
573
574        let pk_share = key_package.public_key();
575        let valid = verify_share(
576            share,
577            commitment,
578            &pk_share,
579            group_public_key,
580            commitments_list,
581            message,
582        )?;
583
584        if !valid {
585            cheaters.push(share.identifier);
586        }
587    }
588
589    Ok(cheaters)
590}
591
592#[cfg(test)]
593#[allow(clippy::unwrap_used, clippy::expect_used)]
594mod tests {
595    use super::*;
596    use crate::threshold::frost::keygen;
597
598    fn setup_2_of_3() -> (keygen::KeyGenOutput, AffinePoint) {
599        let secret = [0x42u8; 32];
600        let output = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
601        let group_pk = output.group_public_key;
602        (output, group_pk)
603    }
604
605    // ─── Full 2-of-3 Round-Trip ────────────────────────────────
606
607    #[test]
608    fn test_frost_2_of_3_roundtrip() {
609        let (kgen, group_pk) = setup_2_of_3();
610        let msg = b"frost threshold message";
611
612        // Round 1: participants 1 and 2 commit
613        let nonce1 = commit(&kgen.key_packages[0]).unwrap();
614        let nonce2 = commit(&kgen.key_packages[1]).unwrap();
615        let commitments = vec![nonce1.commitments.clone(), nonce2.commitments.clone()];
616
617        // Round 2: each signs
618        let share1 = sign(&kgen.key_packages[0], nonce1, &commitments, msg).unwrap();
619        let share2 = sign(&kgen.key_packages[1], nonce2, &commitments, msg).unwrap();
620
621        // Aggregate
622        let sig = aggregate(&commitments, &[share1, share2], &group_pk, msg).unwrap();
623        assert_eq!(sig.to_bytes().len(), 65); // 33 (R) + 32 (s)
624
625        // Verify
626        let valid = verify(&sig, &group_pk, msg).unwrap();
627        assert!(valid, "FROST 2-of-3 signature must verify");
628    }
629
630    // ─── Different Signer Subsets ────────────────────────────────
631
632    #[test]
633    fn test_frost_different_participant_subsets() {
634        let (kgen, group_pk) = setup_2_of_3();
635        let msg = b"subset test";
636
637        // Subset {1, 3}
638        let n1 = commit(&kgen.key_packages[0]).unwrap();
639        let n3 = commit(&kgen.key_packages[2]).unwrap();
640        let comms = vec![n1.commitments.clone(), n3.commitments.clone()];
641        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
642        let s3 = sign(&kgen.key_packages[2], n3, &comms, msg).unwrap();
643        let sig = aggregate(&comms, &[s1, s3], &group_pk, msg).unwrap();
644        assert!(
645            verify(&sig, &group_pk, msg).unwrap(),
646            "subset {{1,3}} must verify"
647        );
648    }
649
650    // ─── Share Verification (Identifiable Abort) ────────────────
651
652    #[test]
653    fn test_frost_verify_share() {
654        let (kgen, group_pk) = setup_2_of_3();
655        let msg = b"share verify test";
656
657        let n1 = commit(&kgen.key_packages[0]).unwrap();
658        let n2 = commit(&kgen.key_packages[1]).unwrap();
659        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
660        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
661
662        // Verify participant 1's share
663        let pk1 = kgen.key_packages[0].public_key();
664        let valid = verify_share(&s1, &comms[0], &pk1, &group_pk, &comms, msg).unwrap();
665        assert!(valid, "valid share must verify");
666    }
667
668    // ─── Wrong Message Fails ────────────────────────────────────
669
670    #[test]
671    fn test_frost_wrong_message_fails() {
672        let (kgen, group_pk) = setup_2_of_3();
673        let msg = b"correct msg";
674
675        let n1 = commit(&kgen.key_packages[0]).unwrap();
676        let n2 = commit(&kgen.key_packages[1]).unwrap();
677        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
678        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
679        let s2 = sign(&kgen.key_packages[1], n2, &comms, msg).unwrap();
680        let sig = aggregate(&comms, &[s1, s2], &group_pk, msg).unwrap();
681
682        let wrong = verify(&sig, &group_pk, b"wrong msg").unwrap();
683        assert!(!wrong, "wrong message must fail verification");
684    }
685
686    // ─── Different Messages → Different Signatures ──────────────
687
688    #[test]
689    fn test_frost_different_messages_different_sigs() {
690        let (kgen, group_pk) = setup_2_of_3();
691
692        let make_sig = |m: &[u8]| -> Vec<u8> {
693            let n1 = commit(&kgen.key_packages[0]).unwrap();
694            let n2 = commit(&kgen.key_packages[1]).unwrap();
695            let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
696            let s1 = sign(&kgen.key_packages[0], n1, &comms, m).unwrap();
697            let s2 = sign(&kgen.key_packages[1], n2, &comms, m).unwrap();
698            aggregate(&comms, &[s1, s2], &group_pk, m)
699                .unwrap()
700                .to_bytes()
701        };
702
703        let sig_a = make_sig(b"message A");
704        let sig_b = make_sig(b"message B");
705        assert_ne!(sig_a, sig_b);
706    }
707
708    // ─── VSS Commitment Verification ────────────────────────────
709
710    #[test]
711    fn test_frost_vss_commitments_verify() {
712        let (kgen, _) = setup_2_of_3();
713        for pkg in &kgen.key_packages {
714            assert!(
715                kgen.vss_commitments
716                    .verify_share(pkg.identifier, pkg.secret_share()),
717                "VSS share must verify for participant {}",
718                pkg.identifier
719            );
720        }
721    }
722
723    // ─── Aggregate Rejects Insufficient Shares ──────────────────
724
725    #[test]
726    fn test_frost_aggregate_rejects_single_share() {
727        let (kgen, group_pk) = setup_2_of_3();
728        let msg = b"need 2";
729
730        let n1 = commit(&kgen.key_packages[0]).unwrap();
731        let comms = vec![n1.commitments.clone()];
732        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
733
734        // Only 1 share — must fail (need at least t=2)
735        assert!(aggregate(&comms, &[s1], &group_pk, msg).is_err());
736    }
737
738    // ─── Deterministic Key Generation ───────────────────────────
739
740    #[test]
741    fn test_frost_keygen_deterministic() {
742        let secret = [0x42u8; 32];
743        let out1 = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
744        let out2 = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
745        assert_eq!(
746            out1.group_public_key.to_encoded_point(true).as_bytes(),
747            out2.group_public_key.to_encoded_point(true).as_bytes()
748        );
749    }
750
751    // ─── Identifiable Abort Tests ───────────────────────────────
752
753    #[test]
754    fn test_identify_misbehaving_all_honest() {
755        let (kgen, group_pk) = setup_2_of_3();
756        let msg = b"identifiable abort honest";
757
758        let n1 = commit(&kgen.key_packages[0]).unwrap();
759        let n2 = commit(&kgen.key_packages[1]).unwrap();
760        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
761        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
762        let s2 = sign(&kgen.key_packages[1], n2, &comms, msg).unwrap();
763
764        let cheaters =
765            identify_misbehaving(&[s1, s2], &comms, &kgen.key_packages, &group_pk, msg).unwrap();
766        assert!(cheaters.is_empty(), "no cheaters expected");
767    }
768
769    #[test]
770    fn test_identify_misbehaving_tampered_share() {
771        let (kgen, group_pk) = setup_2_of_3();
772        let msg = b"identifiable abort tampered";
773
774        let n1 = commit(&kgen.key_packages[0]).unwrap();
775        let n2 = commit(&kgen.key_packages[1]).unwrap();
776        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
777        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
778        let s2 = sign(&kgen.key_packages[1], n2, &comms, msg).unwrap();
779
780        // Tamper with participant 2's share
781        let tampered_s2 = SignatureShare {
782            identifier: s2.identifier,
783            share: s2.share + Scalar::ONE, // corrupt!
784        };
785
786        let cheaters = identify_misbehaving(
787            &[s1, tampered_s2],
788            &comms,
789            &kgen.key_packages,
790            &group_pk,
791            msg,
792        )
793        .unwrap();
794        assert_eq!(
795            cheaters,
796            vec![2],
797            "participant 2 should be identified as cheater"
798        );
799    }
800
801    #[test]
802    fn test_identify_misbehaving_both_tampered() {
803        let (kgen, group_pk) = setup_2_of_3();
804        let msg = b"both bad";
805
806        let n1 = commit(&kgen.key_packages[0]).unwrap();
807        let n2 = commit(&kgen.key_packages[1]).unwrap();
808        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
809        let s1 = sign(&kgen.key_packages[0], n1, &comms, msg).unwrap();
810        let s2 = sign(&kgen.key_packages[1], n2, &comms, msg).unwrap();
811
812        let bad1 = SignatureShare {
813            identifier: s1.identifier,
814            share: Scalar::ZERO,
815        };
816        let bad2 = SignatureShare {
817            identifier: s2.identifier,
818            share: Scalar::ZERO,
819        };
820
821        let cheaters =
822            identify_misbehaving(&[bad1, bad2], &comms, &kgen.key_packages, &group_pk, msg)
823                .unwrap();
824        assert_eq!(cheaters.len(), 2, "both participants should be identified");
825    }
826}