newton-core 0.4.16

newton protocol core sdk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Proactive Secret Sharing (PSS) refresh and resharing primitives.
//!
//! Builds on the Feldman VSS infrastructure in [`dealer`] and [`combine`] to
//! support two operations:
//!
//! - **Refresh** (same threshold, same participants): Each operator generates a
//!   random polynomial with zero constant term, distributes sub-shares, and adds
//!   the received deltas to their current share. The master secret (and MPK) are
//!   unchanged, but all shares rotate.
//!
//! - **Reshare** (new threshold or new participant set): Each current operator
//!   generates a polynomial with their current share as the constant term and
//!   distributes sub-shares to the incoming participants. Incoming participants
//!   Lagrange-interpolate the received sub-shares to obtain their new share.
//!   The master secret (and MPK) are again unchanged.

use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::EdwardsPoint, scalar::Scalar};
use rand_core::OsRng;

use super::{combine, dealer};
use crate::crypto::error::CryptoError;

/// Generate a random refresh polynomial of degree `threshold - 1` with a zero
/// constant term.
///
/// When every operator generates such a polynomial, evaluates it at each peer's
/// index, and distributes the resulting sub-shares, the sum of all sub-shares
/// for a given index forms a "delta" that can be added to the current share
/// without changing the master secret (because all constant terms are zero, so
/// they cancel at x = 0).
pub fn generate_refresh_polynomial(threshold: u32) -> Vec<Scalar> {
    let mut rng = OsRng;
    let mut coefficients = Vec::with_capacity(threshold as usize);
    // a_0 = 0 (zero constant term preserves the master secret)
    coefficients.push(Scalar::ZERO);
    for _ in 1..threshold {
        coefficients.push(Scalar::random(&mut rng));
    }
    coefficients
}

/// Generate a reshare polynomial of degree `new_threshold - 1` with the
/// operator's current secret share as the constant term.
///
/// Each current operator creates one such polynomial and evaluates it at each
/// incoming participant's index. The incoming participants then Lagrange-
/// interpolate the received sub-shares to recover their share of the original
/// master secret.
pub fn generate_reshare_polynomial(share: &Scalar, new_threshold: u32) -> Vec<Scalar> {
    let mut rng = OsRng;
    let mut coefficients = Vec::with_capacity(new_threshold as usize);
    // a_0 = operator's current secret share
    coefficients.push(*share);
    for _ in 1..new_threshold {
        coefficients.push(Scalar::random(&mut rng));
    }
    coefficients
}

/// Compute Feldman commitments for a set of polynomial coefficients.
///
/// Returns `C_k = coeff_k * G` for each coefficient, suitable for share
/// verification via [`verify_refresh_share`].
pub fn polynomial_commitments(coefficients: &[Scalar]) -> Vec<EdwardsPoint> {
    coefficients.iter().map(|c| c * ED25519_BASEPOINT_POINT).collect()
}

/// Evaluate a polynomial at a 1-based participant index.
///
/// Thin wrapper around [`dealer::evaluate_polynomial`] that converts the index
/// to a [`Scalar`].
pub fn evaluate_at(coefficients: &[Scalar], index: u32) -> Scalar {
    let x = Scalar::from(index);
    dealer::evaluate_polynomial(coefficients, &x)
}

/// Add received refresh deltas to the current share.
///
/// `new_share = current_share + Σ deltas`
///
/// Each delta is the sum of sub-shares received from every peer's refresh
/// polynomial evaluated at this operator's index.
pub fn accumulate_refresh(current_share: &Scalar, deltas: &[Scalar]) -> Scalar {
    let delta_sum: Scalar = deltas.iter().sum();
    current_share + delta_sum
}

/// Lagrange-interpolate sub-shares from current participants to produce a share
/// for an incoming participant during resharing.
///
/// Each entry in `sub_shares` is `(current_participant_index, sub_share_value)`
/// where the sub-share was computed by that current participant's reshare
/// polynomial evaluated at the incoming participant's index.
///
/// # Arguments
///
/// * `sub_shares` - `(current_index, value)` pairs from current operators
/// * `current_participants` - Full set of current participant indices used in resharing
///
/// # Errors
///
/// Returns an error if Lagrange interpolation fails (e.g., duplicate indices).
pub fn combine_reshare(sub_shares: &[(u32, Scalar)], old_participants: &[u32]) -> Result<Scalar, CryptoError> {
    let mut new_share = Scalar::ZERO;
    for &(old_index, sub_share_value) in sub_shares {
        let lambda = combine::lagrange_coefficient(old_index, old_participants)?;
        new_share += lambda * sub_share_value;
    }
    Ok(new_share)
}

/// Verify a refresh or reshare sub-share against Feldman commitments.
///
/// Checks the Feldman verification equation:
///
/// `share_value * G == Σ_j C_j * index^j`
///
/// where `C_j` are the polynomial commitments and `index` is the recipient's
/// 1-based participant index.
pub fn verify_refresh_share(share_value: &Scalar, recipient_index: u32, commitments: &[EdwardsPoint]) -> bool {
    let x = Scalar::from(recipient_index);
    let mut x_pow = Scalar::ONE;
    let mut expected = EdwardsPoint::default(); // identity

    for c_j in commitments {
        expected += x_pow * c_j;
        x_pow *= x;
    }

    // Constant-time comparison
    (share_value * ED25519_BASEPOINT_POINT).compress() == expected.compress()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        crypto::hpke,
        dkg::{
            combine::{compute_partial_decryption, montgomery_to_edwards, threshold_decrypt},
            dealer,
            types::{KeyShare, ThresholdConfig},
        },
    };
    use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::EdwardsPoint, scalar::Scalar};
    use std::collections::HashMap;

    #[test]
    fn refresh_polynomial_has_zero_constant() {
        for threshold in 1..=5 {
            let poly = generate_refresh_polynomial(threshold);
            assert_eq!(poly.len(), threshold as usize);
            assert_eq!(
                poly[0],
                Scalar::ZERO,
                "constant term must be zero for threshold={threshold}"
            );
        }
    }

    #[test]
    fn refresh_preserves_secret_2_of_3() {
        let config = ThresholdConfig { threshold: 2, total: 3 };
        let (tpk, _commitment, shares) = dealer::generate_shares(config).unwrap();
        let original_mpk = tpk.edwards;

        // Encrypt something to the original MPK
        let pk = hpke::HpkePublicKey::from_bytes(&tpk.hpke_public_key).unwrap();
        let plaintext = b"PSS refresh test payload";
        let aad = b"refresh-context";
        let (enc, ct) = hpke::encrypt(&pk, plaintext, aad).unwrap();

        // Each operator generates a refresh polynomial and distributes sub-shares
        let mut refresh_polys: Vec<Vec<Scalar>> = Vec::new();
        for _ in 0..config.total {
            refresh_polys.push(generate_refresh_polynomial(config.threshold));
        }

        // Each operator accumulates deltas from all peers
        let mut new_shares: Vec<KeyShare> = Vec::new();
        for recipient in &shares {
            let mut deltas = Vec::new();
            for poly in &refresh_polys {
                deltas.push(evaluate_at(poly, recipient.index));
            }
            let new_secret = accumulate_refresh(&recipient.secret_share, &deltas);
            let new_public = new_secret * ED25519_BASEPOINT_POINT;
            new_shares.push(KeyShare {
                index: recipient.index,
                secret_share: new_secret,
                public_share: new_public,
            });
        }

        // Verify reconstructed MPK from new shares equals original MPK
        let indices: Vec<u32> = new_shares.iter().map(|s| s.index).collect();
        let mut reconstructed_mpk = EdwardsPoint::default();
        for share in &new_shares {
            let lambda = combine::lagrange_coefficient(share.index, &indices).unwrap();
            reconstructed_mpk += lambda * share.public_share;
        }
        assert_eq!(
            reconstructed_mpk.compress(),
            original_mpk.compress(),
            "MPK must be unchanged after refresh"
        );

        // Verify decryption still works with new shares (using shares 1, 3)
        let public_shares: HashMap<u32, EdwardsPoint> = new_shares.iter().map(|s| (s.index, s.public_share)).collect();

        let enc_bytes: [u8; 32] = enc[..32].try_into().unwrap();
        let enc_edwards = montgomery_to_edwards(&enc_bytes).unwrap();

        let partials: Vec<_> = [&new_shares[0], &new_shares[2]]
            .iter()
            .map(|s| compute_partial_decryption(s.index, &s.secret_share, &enc_edwards))
            .collect();

        let recovered = threshold_decrypt(
            &partials,
            &enc_bytes,
            &tpk.hpke_public_key,
            &ct,
            aad,
            &public_shares,
            config.threshold,
        )
        .unwrap();

        assert_eq!(recovered[..], plaintext[..]);
    }

    #[test]
    fn refresh_share_verification() {
        let poly = generate_refresh_polynomial(3);
        let commitments = polynomial_commitments(&poly);

        // Valid share should verify
        let share_val = evaluate_at(&poly, 2);
        assert!(
            verify_refresh_share(&share_val, 2, &commitments),
            "valid share must verify"
        );

        // Tampered share should fail
        let tampered = share_val + Scalar::ONE;
        assert!(
            !verify_refresh_share(&tampered, 2, &commitments),
            "tampered share must fail verification"
        );

        // Wrong index should fail
        assert!(
            !verify_refresh_share(&share_val, 3, &commitments),
            "wrong index must fail verification"
        );
    }

    #[test]
    fn reshare_to_larger_set() {
        // 2-of-2 -> 2-of-3
        let old_config = ThresholdConfig { threshold: 2, total: 2 };
        let (tpk, _commitment, old_shares) = dealer::generate_shares(old_config).unwrap();
        let original_mpk = tpk.edwards;

        // Encrypt to original MPK
        let pk = hpke::HpkePublicKey::from_bytes(&tpk.hpke_public_key).unwrap();
        let plaintext = b"reshare expansion test";
        let aad = b"reshare-context";
        let (enc, ct) = hpke::encrypt(&pk, plaintext, aad).unwrap();

        let new_threshold = 2u32;
        let new_total = 3u32;
        let old_participants: Vec<u32> = old_shares.iter().map(|s| s.index).collect();

        // Each old operator generates a reshare polynomial
        let reshare_polys: Vec<Vec<Scalar>> = old_shares
            .iter()
            .map(|s| generate_reshare_polynomial(&s.secret_share, new_threshold))
            .collect();

        // Each new participant collects sub-shares from all old operators
        let mut new_shares: Vec<KeyShare> = Vec::new();
        for new_index in 1..=new_total {
            let sub_shares: Vec<(u32, Scalar)> = old_shares
                .iter()
                .zip(reshare_polys.iter())
                .map(|(old_share, poly)| {
                    let sub_share = evaluate_at(poly, new_index);
                    (old_share.index, sub_share)
                })
                .collect();

            let new_secret = combine_reshare(&sub_shares, &old_participants).unwrap();
            let new_public = new_secret * ED25519_BASEPOINT_POINT;
            new_shares.push(KeyShare {
                index: new_index,
                secret_share: new_secret,
                public_share: new_public,
            });
        }

        // Verify MPK unchanged
        let new_indices: Vec<u32> = new_shares.iter().map(|s| s.index).collect();
        let mut reconstructed_mpk = EdwardsPoint::default();
        for share in &new_shares {
            let lambda = combine::lagrange_coefficient(share.index, &new_indices).unwrap();
            reconstructed_mpk += lambda * share.public_share;
        }
        assert_eq!(
            reconstructed_mpk.compress(),
            original_mpk.compress(),
            "MPK must be unchanged after resharing to larger set"
        );

        // Verify decryption works with any 2-of-3 subset
        let public_shares: HashMap<u32, EdwardsPoint> = new_shares.iter().map(|s| (s.index, s.public_share)).collect();

        let enc_bytes: [u8; 32] = enc[..32].try_into().unwrap();
        let enc_edwards = montgomery_to_edwards(&enc_bytes).unwrap();

        let subsets: Vec<Vec<usize>> = vec![vec![0, 1], vec![0, 2], vec![1, 2]];
        for subset in &subsets {
            let partials: Vec<_> = subset
                .iter()
                .map(|&i| compute_partial_decryption(new_shares[i].index, &new_shares[i].secret_share, &enc_edwards))
                .collect();

            let recovered = threshold_decrypt(
                &partials,
                &enc_bytes,
                &tpk.hpke_public_key,
                &ct,
                aad,
                &public_shares,
                new_threshold,
            )
            .unwrap();

            assert_eq!(
                recovered[..],
                plaintext[..],
                "decryption failed for subset {:?}",
                subset
            );
        }
    }

    #[test]
    fn reshare_to_smaller_set() {
        // 2-of-3 -> 2-of-2: only t=2 old operators needed for resharing
        let old_config = ThresholdConfig { threshold: 2, total: 3 };
        let (tpk, _commitment, old_shares) = dealer::generate_shares(old_config).unwrap();
        let original_mpk = tpk.edwards;

        // Encrypt to original MPK
        let pk = hpke::HpkePublicKey::from_bytes(&tpk.hpke_public_key).unwrap();
        let plaintext = b"reshare shrink test";
        let aad = b"shrink-context";
        let (enc, ct) = hpke::encrypt(&pk, plaintext, aad).unwrap();

        let new_threshold = 2u32;
        let new_total = 2u32;

        // Use only the first t=2 old operators for resharing
        let participating_old: Vec<&KeyShare> = old_shares.iter().take(old_config.threshold as usize).collect();
        let old_participants: Vec<u32> = participating_old.iter().map(|s| s.index).collect();

        let reshare_polys: Vec<Vec<Scalar>> = participating_old
            .iter()
            .map(|s| generate_reshare_polynomial(&s.secret_share, new_threshold))
            .collect();

        // Each new participant collects sub-shares from participating old operators
        let mut new_shares: Vec<KeyShare> = Vec::new();
        for new_index in 1..=new_total {
            let sub_shares: Vec<(u32, Scalar)> = participating_old
                .iter()
                .zip(reshare_polys.iter())
                .map(|(old_share, poly)| {
                    let sub_share = evaluate_at(poly, new_index);
                    (old_share.index, sub_share)
                })
                .collect();

            let new_secret = combine_reshare(&sub_shares, &old_participants).unwrap();
            let new_public = new_secret * ED25519_BASEPOINT_POINT;
            new_shares.push(KeyShare {
                index: new_index,
                secret_share: new_secret,
                public_share: new_public,
            });
        }

        // Verify MPK unchanged
        let new_indices: Vec<u32> = new_shares.iter().map(|s| s.index).collect();
        let mut reconstructed_mpk = EdwardsPoint::default();
        for share in &new_shares {
            let lambda = combine::lagrange_coefficient(share.index, &new_indices).unwrap();
            reconstructed_mpk += lambda * share.public_share;
        }
        assert_eq!(
            reconstructed_mpk.compress(),
            original_mpk.compress(),
            "MPK must be unchanged after resharing to smaller set"
        );

        // Verify decryption works with 2-of-2
        let public_shares: HashMap<u32, EdwardsPoint> = new_shares.iter().map(|s| (s.index, s.public_share)).collect();

        let enc_bytes: [u8; 32] = enc[..32].try_into().unwrap();
        let enc_edwards = montgomery_to_edwards(&enc_bytes).unwrap();

        let partials: Vec<_> = new_shares
            .iter()
            .map(|s| compute_partial_decryption(s.index, &s.secret_share, &enc_edwards))
            .collect();

        let recovered = threshold_decrypt(
            &partials,
            &enc_bytes,
            &tpk.hpke_public_key,
            &ct,
            aad,
            &public_shares,
            new_threshold,
        )
        .unwrap();

        assert_eq!(recovered[..], plaintext[..]);
    }
}