Skip to main content

confium_crypto_vss/
pedersen_vss.rs

1//! Pedersen VSS — verifiable secret sharing with hiding commitments.
2
3use getrandom::SysRng;
4use p256::elliptic_curve::rand_core::UnwrapErr;
5use p256::elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
6use p256::elliptic_curve::{Field, PrimeField};
7use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest as _, Sha256};
10
11/// Pedersen VSS commitment: (C_i, D_i) = (g^{a_i} * h^{r_i}, h^{r_i}).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PedersenCommitment {
14    pub c_points_hex: Vec<String>,
15    pub d_points_hex: Vec<String>,
16}
17
18/// A Pedersen share: (x, y, y_r) where y = f(x), y_r = r(x).
19/// The secret `value` and `randomness` fields are zeroized on drop.
20#[derive(Debug, Clone)]
21pub struct PedersenShare {
22    pub party_idx: u32,
23    pub value: Scalar,
24    pub randomness: Scalar,
25}
26
27impl Drop for PedersenShare {
28    fn drop(&mut self) {
29        use zeroize::Zeroize;
30        self.value.zeroize();
31        self.randomness.zeroize();
32    }
33}
34
35/// Second generator h = g^alpha for some unknown alpha.
36#[derive(Debug, Clone)]
37pub struct PedersenParams {
38    pub h: AffinePoint,
39}
40
41impl PedersenParams {
42    /// Generate h = g^alpha for random alpha (trapdoor discarded).
43    pub fn generate() -> Self {
44        let alpha = Scalar::random(&mut UnwrapErr(SysRng));
45        let h = (ProjectivePoint::GENERATOR * alpha).to_affine();
46        Self { h }
47    }
48}
49
50/// Deal a secret using Pedersen VSS.
51/// Returns (commitments, shares) for T-of-N.
52pub fn deal(
53    secret: &Scalar,
54    threshold: u32,
55    party_count: u32,
56    params: &PedersenParams,
57) -> (PedersenCommitment, Vec<PedersenShare>) {
58    // Two polynomials: f(x) for secret, r(x) for randomness
59    let f_coeffs: Vec<Scalar> = (0..threshold)
60        .map(|i| {
61            if i == 0 {
62                *secret
63            } else {
64                Scalar::random(&mut UnwrapErr(SysRng))
65            }
66        })
67        .collect();
68    let r_coeffs: Vec<Scalar> = (0..threshold)
69        .map(|_| Scalar::random(&mut UnwrapErr(SysRng)))
70        .collect();
71
72    // Commitments: C_i = g^{f_i} * h^{r_i}, D_i = h^{r_i}
73    let mut c_points = Vec::with_capacity(threshold as usize);
74    let mut d_points = Vec::with_capacity(threshold as usize);
75    for i in 0..threshold as usize {
76        let g_fi = ProjectivePoint::GENERATOR * f_coeffs[i];
77        let h_ri = ProjectivePoint::from(params.h) * r_coeffs[i];
78        let c_i = (g_fi + h_ri).to_affine();
79        let d_i = h_ri.to_affine();
80        c_points.push(encode_point(&c_i));
81        d_points.push(encode_point(&d_i));
82    }
83
84    // Shares: f(j), r(j) for j = 1..=N
85    let shares: Vec<PedersenShare> = (1..=party_count)
86        .map(|j| PedersenShare {
87            party_idx: j,
88            value: eval_poly(&f_coeffs, j),
89            randomness: eval_poly(&r_coeffs, j),
90        })
91        .collect();
92
93    (
94        PedersenCommitment {
95            c_points_hex: c_points,
96            d_points_hex: d_points,
97        },
98        shares,
99    )
100}
101
102/// Verify a Pedersen share against commitments.
103/// Checks: g^{f(j)} * h^{r(j)} == product(C_i^{j^i}).
104pub fn verify_share(
105    share: &PedersenShare,
106    commitment: &PedersenCommitment,
107    params: &PedersenParams,
108) -> bool {
109    // Compute g^{f(j)} * h^{r(j)}
110    let lhs = (ProjectivePoint::GENERATOR * share.value
111        + ProjectivePoint::from(params.h) * share.randomness)
112        .to_affine();
113
114    // Compute product(C_i^{j^i})
115    let mut rhs = ProjectivePoint::IDENTITY;
116    let j = share.party_idx;
117    let mut j_pow = Scalar::ONE;
118    for i in 0..commitment.c_points_hex.len() {
119        if let Some(c_i) = decode_point(&commitment.c_points_hex[i]) {
120            rhs += ProjectivePoint::from(c_i) * j_pow;
121        }
122        let j_scalar = u32_to_scalar(j);
123        j_pow *= j_scalar;
124    }
125
126    lhs == rhs.to_affine()
127}
128
129/// Extract the joint public key (C_0 without randomness hiding).
130pub fn joint_public_key(commitment: &PedersenCommitment) -> Option<AffinePoint> {
131    if commitment.c_points_hex.is_empty() {
132        return None;
133    }
134    // C_0 = g^{secret} * h^{r_0}
135    // The actual public key is g^{secret}, but we can't separate it without knowing r_0
136    // For a committed public key, use C_0 / D_0 = g^{secret}
137    if commitment.d_points_hex.is_empty() {
138        return None;
139    }
140    let c0 = decode_point(&commitment.c_points_hex[0])?;
141    let d0 = decode_point(&commitment.d_points_hex[0])?;
142    let pk = ProjectivePoint::from(c0) - ProjectivePoint::from(d0);
143    Some(pk.to_affine())
144}
145
146fn eval_poly(coeffs: &[Scalar], x: u32) -> Scalar {
147    let x_scalar = u32_to_scalar(x);
148    let mut result = Scalar::ZERO;
149    let mut x_pow = Scalar::ONE;
150    for c in coeffs {
151        result += c * &x_pow;
152        x_pow *= x_scalar;
153    }
154    result
155}
156
157fn u32_to_scalar(v: u32) -> Scalar {
158    let mut arr = [0u8; 32];
159    arr[28..32].copy_from_slice(&v.to_be_bytes());
160    loop {
161        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(arr))) {
162            return s;
163        }
164        arr = {
165            let mut h = Sha256::new();
166            h.update(b"confium-scalar-reduce-v1");
167            h.update(arr);
168            h.finalize().into()
169        };
170    }
171}
172
173fn encode_point(p: &AffinePoint) -> String {
174    hex::encode(p.to_sec1_point(true).as_bytes())
175}
176
177fn decode_point(hex_str: &str) -> Option<AffinePoint> {
178    let bytes = hex::decode(hex_str).ok()?;
179    let encoded =
180        p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(&bytes).ok()?;
181    Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&encoded))
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn deal_and_verify() {
190        let params = PedersenParams::generate();
191        let secret = Scalar::random(&mut UnwrapErr(SysRng));
192        let (commitment, shares) = deal(&secret, 3, 5, &params);
193        for share in &shares {
194            assert!(
195                verify_share(share, &commitment, &params),
196                "party {}",
197                share.party_idx
198            );
199        }
200    }
201
202    #[test]
203    fn tampered_share_rejected() {
204        let params = PedersenParams::generate();
205        let secret = Scalar::random(&mut UnwrapErr(SysRng));
206        let (commitment, mut shares) = deal(&secret, 2, 3, &params);
207        shares[0].value += Scalar::ONE;
208        assert!(!verify_share(&shares[0], &commitment, &params));
209    }
210
211    #[test]
212    fn joint_public_key_extracted() {
213        let params = PedersenParams::generate();
214        let secret = Scalar::random(&mut UnwrapErr(SysRng));
215        let (commitment, _) = deal(&secret, 2, 3, &params);
216        let pk = joint_public_key(&commitment).unwrap();
217        // g^{secret} == pk
218        let expected = (ProjectivePoint::GENERATOR * secret).to_affine();
219        assert_eq!(pk, expected);
220    }
221
222    #[test]
223    fn different_secrets_different_commitments() {
224        let params = PedersenParams::generate();
225        let s1 = Scalar::random(&mut UnwrapErr(SysRng));
226        let s2 = Scalar::random(&mut UnwrapErr(SysRng));
227        let (c1, _) = deal(&s1, 2, 3, &params);
228        let (c2, _) = deal(&s2, 2, 3, &params);
229        assert_ne!(c1.c_points_hex[0], c2.c_points_hex[0]);
230    }
231
232    #[test]
233    fn threshold_one_works() {
234        let params = PedersenParams::generate();
235        let secret = Scalar::random(&mut UnwrapErr(SysRng));
236        let (commitment, shares) = deal(&secret, 1, 3, &params);
237        assert_eq!(shares.len(), 3);
238        for share in &shares {
239            assert!(verify_share(share, &commitment, &params));
240        }
241    }
242}