Skip to main content

confium_tc_core/
nonce.rs

1//! Threshold nonce derivation — deterministic per-party nonce shares.
2//!
3//! Each party derives their nonce share deterministically from:
4//! - Their private key share (secret scalar)
5//! - The message hash being signed
6//! - Their party index
7//!
8//! This eliminates the interactive nonce commitment round (CMP20/GG18
9//! round 1) and prevents nonce reuse attacks. The full nonce is the
10//! sum of all T nonce shares, reconstructed via Lagrange interpolation.
11//!
12//! ## Security note
13//!
14//! This is a simplified deterministic derivation suitable for testing
15//! and non-interactive signing modes. Production CMP20/GG18 signing
16//! uses interactive nonce generation for stronger security guarantees.
17
18use hmac::{Hmac, KeyInit, Mac};
19use p256::elliptic_curve::PrimeField;
20use p256::{FieldBytes, Scalar};
21use sha2::Sha256;
22
23type HmacSha256 = Hmac<Sha256>;
24
25/// Derive a deterministic nonce share for a party. The nonce is
26/// derived from the party's secret scalar, the message hash, and
27/// their party index via HMAC-SHA256, reduced mod the P-256 group
28/// order.
29///
30/// The same inputs always produce the same nonce share — enabling
31/// deterministic signing and non-interactive nonce generation.
32pub fn derive_nonce_share(
33    secret_share: &Scalar,
34    message_hash: &[u8; 32],
35    party_idx: u32,
36) -> Scalar {
37    let mut mac =
38        HmacSha256::new_from_slice(&secret_share.to_repr()).expect("HMAC accepts any key length");
39    mac.update(message_hash);
40    mac.update(&party_idx.to_be_bytes());
41    let result = mac.finalize().into_bytes();
42
43    let mut nonce_bytes = [0u8; 32];
44    nonce_bytes.copy_from_slice(&result);
45
46    reduce_mod_order(&nonce_bytes)
47}
48
49/// Sum T nonce shares (via Lagrange-weighted addition) to get the
50/// full nonce. In threshold signing, the full nonce k = sum of
51/// Lagrange-weighted nonce shares.
52///
53/// This is a simple sum — Lagrange weighting is applied at the
54/// signature level, not the nonce level, in most threshold ECDSA
55/// protocols.
56pub fn sum_nonce_shares(shares: &[Scalar]) -> Scalar {
57    shares.iter().fold(Scalar::ZERO, |acc, s| acc + s)
58}
59
60/// Derive nonce shares for all parties and return the full nonce
61/// (their sum).
62pub fn derive_full_nonce(
63    secret_shares: &[Scalar],
64    message_hash: &[u8; 32],
65    party_indices: &[u32],
66) -> Scalar {
67    assert_eq!(secret_shares.len(), party_indices.len());
68    let shares: Vec<Scalar> = secret_shares
69        .iter()
70        .zip(party_indices.iter())
71        .map(|(s, &idx)| derive_nonce_share(s, message_hash, idx))
72        .collect();
73    sum_nonce_shares(&shares)
74}
75
76fn reduce_mod_order(bytes: &[u8; 32]) -> Scalar {
77    let fb = FieldBytes::from(*bytes);
78    let ct = Scalar::from_repr(fb);
79    Option::<Scalar>::from(ct).unwrap_or_else(|| {
80        // If the value is >= n, subtract 1 and try again. This is a
81        // simple rejection-free reduction that ensures a non-zero
82        // result in the valid range [1, n-1].
83        let mut adjusted = *bytes;
84        adjusted[0] = adjusted[0].wrapping_sub(1);
85        let fb2 = FieldBytes::from(adjusted);
86        let ct2 = Scalar::from_repr(fb2);
87        Option::<Scalar>::from(ct2).unwrap_or(Scalar::ONE)
88    })
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use p256::elliptic_curve::Field;
95    use p256::elliptic_curve::rand_core::UnwrapErr;
96
97    fn random_scalar() -> Scalar {
98        Scalar::random(&mut UnwrapErr(getrandom::SysRng))
99    }
100
101    #[test]
102    fn derivation_is_deterministic() {
103        let secret = random_scalar();
104        let msg = [0x42u8; 32];
105        let n1 = derive_nonce_share(&secret, &msg, 1);
106        let n2 = derive_nonce_share(&secret, &msg, 1);
107        assert_eq!(n1, n2);
108    }
109
110    #[test]
111    fn different_party_indices_differ() {
112        let secret = random_scalar();
113        let msg = [0x42u8; 32];
114        let n1 = derive_nonce_share(&secret, &msg, 1);
115        let n2 = derive_nonce_share(&secret, &msg, 2);
116        assert_ne!(n1, n2);
117    }
118
119    #[test]
120    fn different_messages_differ() {
121        let secret = random_scalar();
122        let m1 = [0x01u8; 32];
123        let m2 = [0x02u8; 32];
124        let n1 = derive_nonce_share(&secret, &m1, 1);
125        let n2 = derive_nonce_share(&secret, &m2, 1);
126        assert_ne!(n1, n2);
127    }
128
129    #[test]
130    fn different_secrets_differ() {
131        let s1 = random_scalar();
132        let s2 = random_scalar();
133        let msg = [0x42u8; 32];
134        let n1 = derive_nonce_share(&s1, &msg, 1);
135        let n2 = derive_nonce_share(&s2, &msg, 1);
136        assert_ne!(n1, n2);
137    }
138
139    #[test]
140    fn nonce_share_is_nonzero() {
141        let secret = random_scalar();
142        let msg = [0x42u8; 32];
143        let nonce = derive_nonce_share(&secret, &msg, 1);
144        assert_ne!(nonce, Scalar::ZERO);
145    }
146
147    #[test]
148    fn sum_nonce_shares_works() {
149        let s1 = random_scalar();
150        let s2 = random_scalar();
151        let expected = s1 + s2;
152        assert_eq!(sum_nonce_shares(&[s1, s2]), expected);
153    }
154
155    #[test]
156    fn sum_empty_is_zero() {
157        assert_eq!(sum_nonce_shares(&[]), Scalar::ZERO);
158    }
159
160    #[test]
161    fn full_nonce_is_sum_of_shares() {
162        let secrets = vec![random_scalar(), random_scalar(), random_scalar()];
163        let indices = vec![1u32, 2, 3];
164        let msg = [0xAAu8; 32];
165
166        let shares: Vec<Scalar> = secrets
167            .iter()
168            .zip(indices.iter())
169            .map(|(s, &i)| derive_nonce_share(s, &msg, i))
170            .collect();
171        let expected = sum_nonce_shares(&shares);
172        let actual = derive_full_nonce(&secrets, &msg, &indices);
173        assert_eq!(actual, expected);
174    }
175
176    #[test]
177    fn nonce_is_valid_scalar() {
178        let secret = random_scalar();
179        let msg = [0xFFu8; 32];
180        let nonce = derive_nonce_share(&secret, &msg, 1);
181        // Just verify it's representable (i.e., < n)
182        let _bytes = nonce.to_repr();
183    }
184
185    #[test]
186    fn reduce_mod_order_handles_large_input() {
187        let max_bytes = [0xFFu8; 32];
188        let result = reduce_mod_order(&max_bytes);
189        assert_ne!(result, Scalar::ZERO);
190    }
191}