Skip to main content

confium_crypto_vss/
vss.rs

1//! Verifiable Secret Sharing (VSS) — Feldman VSS over P-256.
2//!
3//! Allows shareholders to verify their share is valid (lies on the
4//! committed polynomial) without revealing it. Used by CMP20, FROST,
5//! and GG18 internally.
6//!
7//! ## Feldman VSS
8//!
9//! The dealer picks a random polynomial `f(x) = a_0 + a_1*x + ... + a_{T-1}*x^{T-1}`
10//! where `a_0` is the secret. They publish commitments `C_i = g^{a_i}`
11//! for each coefficient. Each shareholder with index `j` receives
12//! share `f(j)`. They verify: `g^{f(j)} == product(C_i^{j^i})`.
13
14use p256::FieldBytes;
15use p256::elliptic_curve::PrimeField;
16use p256::elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
17use p256::{AffinePoint, ProjectivePoint, Scalar};
18use sha2::{Digest as _, Sha256};
19
20/// Public commitments from a Feldman VSS deal.
21///
22/// `C_0 = g^{a_0}` is the joint public key. The remaining commitments
23/// `C_1...C_{T-1}` allow shareholders to verify their shares.
24#[derive(Debug, Clone)]
25pub struct VssCommitment {
26    /// Commitment to each polynomial coefficient: `C_i = g^{a_i}`.
27    pub commitments: Vec<AffinePoint>,
28}
29
30impl VssCommitment {
31    /// Create a new VSS commitment from coefficient commitments.
32    pub fn new(commitments: Vec<AffinePoint>) -> Self {
33        Self { commitments }
34    }
35
36    /// The threshold T (number of shares needed to reconstruct).
37    pub fn threshold(&self) -> usize {
38        self.commitments.len()
39    }
40
41    /// The joint public key: `C_0 = g^{secret}`.
42    pub fn joint_public_key(&self) -> AffinePoint {
43        self.commitments[0]
44    }
45
46    /// Verify that a share `(party_idx, share)` is consistent with
47    /// the committed polynomial.
48    ///
49    /// Checks: `g^{share} == product_i(C_i^{party_idx^i})`
50    pub fn verify_share(&self, party_idx_1based: u64, share: Scalar) -> bool {
51        let x = u64_to_scalar(party_idx_1based);
52        let lhs = ProjectivePoint::GENERATOR * share;
53
54        let mut rhs = ProjectivePoint::IDENTITY;
55        let mut x_pow = Scalar::ONE;
56        for c in &self.commitments {
57            let c_proj = ProjectivePoint::from(*c);
58            rhs += c_proj * x_pow;
59            x_pow *= x;
60        }
61        lhs == rhs
62    }
63
64    /// Encode commitments as concatenated SEC1 compressed points.
65    pub fn encode(&self) -> Vec<u8> {
66        let mut out = Vec::with_capacity(self.commitments.len() * 33);
67        for c in &self.commitments {
68            let encoded = c.to_sec1_point(true);
69            out.extend_from_slice(encoded.as_bytes());
70        }
71        out
72    }
73
74    /// Decode commitments from concatenated SEC1 compressed points.
75    /// Each point is 33 bytes. Returns `None` if any point is invalid.
76    pub fn decode(bytes: &[u8]) -> Option<Self> {
77        if bytes.is_empty() || bytes.len() % 33 != 0 {
78            return None;
79        }
80        let mut commitments = Vec::with_capacity(bytes.len() / 33);
81        for chunk in bytes.chunks_exact(33) {
82            let point =
83                p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(chunk).ok()?;
84            let affine = Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&point))?;
85            commitments.push(affine);
86        }
87        Some(Self { commitments })
88    }
89}
90
91fn u64_to_scalar(v: u64) -> Scalar {
92    let mut arr = [0u8; 32];
93    arr[24..32].copy_from_slice(&v.to_be_bytes());
94    loop {
95        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(arr))) {
96            return s;
97        }
98        arr = {
99            let mut h = Sha256::new();
100            h.update(b"confium-scalar-reduce-v1");
101            h.update(arr);
102            h.finalize().into()
103        };
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn random_scalar() -> Scalar {
112        use p256::elliptic_curve::Field;
113        use p256::elliptic_curve::rand_core::UnwrapErr;
114        Scalar::random(&mut UnwrapErr(getrandom::SysRng))
115    }
116
117    fn make_commitment_for_polynomial(coeffs: &[Scalar]) -> VssCommitment {
118        let commitments: Vec<AffinePoint> = coeffs
119            .iter()
120            .map(|c| (ProjectivePoint::GENERATOR * c).to_affine())
121            .collect();
122        VssCommitment::new(commitments)
123    }
124
125    fn evaluate_polynomial(coeffs: &[Scalar], x: u64) -> Scalar {
126        let x_scalar = u64_to_scalar(x);
127        let mut result = Scalar::ZERO;
128        let mut x_pow = Scalar::ONE;
129        for c in coeffs {
130            result += c * &x_pow;
131            x_pow *= x_scalar;
132        }
133        result
134    }
135
136    #[test]
137    fn valid_share_verifies() {
138        let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
139        let commitment = make_commitment_for_polynomial(&coeffs);
140        let share = evaluate_polynomial(&coeffs, 1);
141        assert!(commitment.verify_share(1, share));
142    }
143
144    #[test]
145    fn invalid_share_rejected() {
146        let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
147        let commitment = make_commitment_for_polynomial(&coeffs);
148        let bad_share = random_scalar();
149        assert!(!commitment.verify_share(1, bad_share));
150    }
151
152    #[test]
153    fn multiple_party_indices_verify() {
154        let coeffs: Vec<Scalar> = (0..4).map(|_| random_scalar()).collect();
155        let commitment = make_commitment_for_polynomial(&coeffs);
156        for party_idx in 1..=5u64 {
157            let share = evaluate_polynomial(&coeffs, party_idx);
158            assert!(
159                commitment.verify_share(party_idx, share),
160                "party {party_idx} should verify"
161            );
162        }
163    }
164
165    #[test]
166    fn joint_public_key_is_first_commitment() {
167        let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
168        let commitment = make_commitment_for_polynomial(&coeffs);
169        let expected_pk = (ProjectivePoint::GENERATOR * coeffs[0]).to_affine();
170        assert_eq!(commitment.joint_public_key(), expected_pk);
171    }
172
173    #[test]
174    fn threshold_is_commitment_count() {
175        let commitment = make_commitment_for_polynomial(&[random_scalar(); 5]);
176        assert_eq!(commitment.threshold(), 5);
177    }
178
179    #[test]
180    fn encode_decode_round_trips() {
181        let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
182        let commitment = make_commitment_for_polynomial(&coeffs);
183        let encoded = commitment.encode();
184        assert_eq!(encoded.len(), 3 * 33);
185
186        let decoded = VssCommitment::decode(&encoded).unwrap();
187        assert_eq!(decoded.commitments.len(), commitment.commitments.len());
188        for (a, b) in commitment
189            .commitments
190            .iter()
191            .zip(decoded.commitments.iter())
192        {
193            assert_eq!(a, b);
194        }
195    }
196
197    #[test]
198    fn decode_rejects_wrong_length() {
199        assert!(VssCommitment::decode(&[0; 10]).is_none());
200        assert!(VssCommitment::decode(&[0; 34]).is_none());
201    }
202
203    #[test]
204    fn decode_rejects_invalid_point() {
205        assert!(VssCommitment::decode(&[0; 33]).is_none());
206    }
207}