Skip to main content

yip_ca/threshold/
refresh.rs

1use std::collections::BTreeMap;
2
3use frost::keys::{KeyPackage, PublicKeyPackage};
4use frost::Identifier;
5use frost_ed25519 as frost;
6
7use crate::threshold::ThresholdError;
8
9/// Performs a proactive share refresh across all signers while preserving the exact same
10/// group verifying key.
11///
12/// **CVE-2025-58359 Hardening**: Explicitly enforces that `requested_threshold == current.min_signers()`.
13/// Any change to the threshold MUST be performed through a full resharing/DKG ceremony, not an in-place refresh.
14pub fn proactive_refresh(
15    current_key_packages: &BTreeMap<Identifier, KeyPackage>,
16    requested_threshold: u16,
17) -> Result<BTreeMap<Identifier, KeyPackage>, ThresholdError> {
18    if current_key_packages.is_empty() {
19        return Err(ThresholdError::InvalidParams("empty key package set"));
20    }
21
22    let first_kp = current_key_packages.values().next().unwrap();
23    if requested_threshold != *first_kp.min_signers() {
24        return Err(ThresholdError::InvalidParams(
25            "requested threshold does not match active key package threshold (CVE-2025-58359 guard)",
26        ));
27    }
28
29    let identifiers: Vec<Identifier> = current_key_packages.keys().copied().collect();
30    let mut verifying_shares = BTreeMap::new();
31    for (id, kp) in current_key_packages {
32        verifying_shares.insert(*id, *kp.verifying_share());
33    }
34
35    let pub_key_package = PublicKeyPackage::new(
36        verifying_shares,
37        *first_kp.verifying_key(),
38        Some(requested_threshold),
39    );
40
41    let mut rng = rand::rngs::OsRng;
42    let (refreshing_shares, _) =
43        frost::keys::refresh::compute_refreshing_shares(pub_key_package, &identifiers, &mut rng)?;
44
45    let mut refreshed_key_packages = BTreeMap::new();
46    for (id, secret_share) in identifiers.into_iter().zip(refreshing_shares) {
47        let current_kp = &current_key_packages[&id];
48        let refreshed_kp = frost::keys::refresh::refresh_share(secret_share, current_kp)?;
49        refreshed_key_packages.insert(id, refreshed_kp);
50    }
51
52    Ok(refreshed_key_packages)
53}
54
55/// Collaborative lost share reconstruction: allows a threshold subset of surviving signers
56/// to reconstruct a lost participant's `KeyPackage` without ever reconstructing the group secret.
57pub fn repair_lost_share(
58    lost_participant: Identifier,
59    helpers: &[(Identifier, KeyPackage)],
60    pubkey_package: &PublicKeyPackage,
61) -> Result<KeyPackage, ThresholdError> {
62    let helper_ids: Vec<Identifier> = helpers.iter().map(|(id, _)| *id).collect();
63    let mut rng = rand::rngs::OsRng;
64
65    // Part 1: Each helper computes delta shares for all helpers
66    let mut deltas_by_recipient: BTreeMap<Identifier, Vec<frost::keys::repairable::Delta>> =
67        BTreeMap::new();
68
69    for (_id, kp) in helpers {
70        let deltas = frost::keys::repairable::repair_share_part1::<frost_ed25519::Ed25519Sha512, _>(
71            &helper_ids,
72            kp,
73            &mut rng,
74            lost_participant,
75        )?;
76        for (recipient, delta) in deltas {
77            deltas_by_recipient
78                .entry(recipient)
79                .or_default()
80                .push(delta);
81        }
82    }
83
84    // Part 2: Each helper sums its received deltas to produce a sigma
85    let mut sigmas = Vec::new();
86    for id in &helper_ids {
87        let deltas = deltas_by_recipient
88            .remove(id)
89            .ok_or(ThresholdError::InvalidParams("missing helper delta"))?;
90        let sigma = frost::keys::repairable::repair_share_part2(&deltas);
91        sigmas.push(sigma);
92    }
93
94    // Part 3: The recovering participant combines sigmas into its KeyPackage
95    let repaired_kp =
96        frost::keys::repairable::repair_share_part3(&sigmas, lost_participant, pubkey_package)?;
97
98    Ok(repaired_kp)
99}