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