Skip to main content

confium_crypto_vss/
nizk.rs

1//! General-purpose NIZK proof system using Fiat-Shamir transform.
2
3use getrandom::SysRng;
4use p256::elliptic_curve::PrimeField;
5use p256::elliptic_curve::rand_core::{Rng, UnwrapErr};
6use p256::elliptic_curve::sec1::ToSec1Point;
7use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
8use sha2::{Digest, Sha256};
9
10/// Reduce 32 bytes to a scalar by rejection sampling with
11/// re-hashing. Never falls back to a constant: a zero nonce leaks
12/// the secret in the response, and a zero challenge accepts
13/// forgeries, so both must be impossible by construction.
14fn hash_to_scalar(mut fb: [u8; 32]) -> Scalar {
15    loop {
16        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(fb))) {
17            return s;
18        }
19        let mut h = Sha256::new();
20        h.update(b"confium-scalar-reduce-v1");
21        h.update(fb);
22        fb = h.finalize().into();
23    }
24}
25
26/// Sample a uniform nonce: rejection sampling over OS randomness.
27fn random_nonce() -> Scalar {
28    loop {
29        let mut b = [0u8; 32];
30        UnwrapErr(SysRng).fill_bytes(&mut b);
31        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(b))) {
32            return s;
33        }
34    }
35}
36
37/// A generic NIZK proof.
38#[derive(Debug, Clone)]
39pub struct NizkProof {
40    pub commitment: AffinePoint,
41    pub challenge: Scalar,
42    pub response: Scalar,
43}
44
45/// Prove knowledge of a discrete log: know x such that Y = x * G.
46pub fn prove_dlog(secret: &Scalar) -> NizkProof {
47    let nonce = random_nonce();
48
49    let commitment = (ProjectivePoint::GENERATOR * nonce).to_affine();
50    let public = (ProjectivePoint::GENERATOR * secret).to_affine();
51
52    let challenge = fiat_shamir_challenge(&public, &commitment, b"dlog");
53    let response = nonce + challenge * secret;
54
55    NizkProof {
56        commitment,
57        challenge,
58        response,
59    }
60}
61
62/// Verify a DLOG proof.
63pub fn verify_dlog(public: &AffinePoint, proof: &NizkProof) -> bool {
64    let expected_challenge = fiat_shamir_challenge(public, &proof.commitment, b"dlog");
65    if expected_challenge != proof.challenge {
66        return false;
67    }
68    // Check: response * G == commitment + challenge * public
69    let lhs = ProjectivePoint::GENERATOR * proof.response;
70    let rhs =
71        ProjectivePoint::from(proof.commitment) + ProjectivePoint::from(*public) * proof.challenge;
72    lhs == rhs
73}
74
75/// Prove equality of discrete logs: Y1 = x*G1 and Y2 = x*G2 (same x).
76pub fn prove_dlog_equality(
77    secret: &Scalar,
78    g1: &AffinePoint,
79    g2: &AffinePoint,
80) -> (NizkProof, NizkProof) {
81    let nonce = random_nonce();
82
83    let commit1 = (ProjectivePoint::from(*g1) * nonce).to_affine();
84    let commit2 = (ProjectivePoint::from(*g2) * nonce).to_affine();
85
86    let y1 = (ProjectivePoint::from(*g1) * secret).to_affine();
87    let y2 = (ProjectivePoint::from(*g2) * secret).to_affine();
88
89    let challenge = equality_challenge(&y1, &y2, &commit1, &commit2);
90    let response = nonce + challenge * secret;
91
92    (
93        NizkProof {
94            commitment: commit1,
95            challenge,
96            response,
97        },
98        NizkProof {
99            commitment: commit2,
100            challenge,
101            response,
102        },
103    )
104}
105
106/// Verify equality of discrete logs.
107pub fn verify_dlog_equality(
108    y1: &AffinePoint,
109    y2: &AffinePoint,
110    g1: &AffinePoint,
111    g2: &AffinePoint,
112    proof1: &NizkProof,
113    proof2: &NizkProof,
114) -> bool {
115    // Same challenge and response
116    if proof1.challenge != proof2.challenge || proof1.response != proof2.response {
117        return false;
118    }
119    let expected_challenge = equality_challenge(y1, y2, &proof1.commitment, &proof2.commitment);
120    if expected_challenge != proof1.challenge {
121        return false;
122    }
123    // Verify both equations
124    let lhs1 = ProjectivePoint::from(*g1) * proof1.response;
125    let rhs1 =
126        ProjectivePoint::from(proof1.commitment) + ProjectivePoint::from(*y1) * proof1.challenge;
127    let lhs2 = ProjectivePoint::from(*g2) * proof2.response;
128    let rhs2 =
129        ProjectivePoint::from(proof2.commitment) + ProjectivePoint::from(*y2) * proof2.challenge;
130    lhs1 == rhs1 && lhs2 == rhs2
131}
132
133fn fiat_shamir_challenge(public: &AffinePoint, commitment: &AffinePoint, domain: &[u8]) -> Scalar {
134    let mut hasher = Sha256::new();
135    hasher.update(b"nizk");
136    hasher.update(domain);
137    hasher.update(public.to_sec1_point(true).as_bytes());
138    hasher.update(commitment.to_sec1_point(true).as_bytes());
139    let fb: [u8; 32] = hasher.finalize().into();
140    hash_to_scalar(fb)
141}
142
143fn equality_challenge(
144    y1: &AffinePoint,
145    y2: &AffinePoint,
146    c1: &AffinePoint,
147    c2: &AffinePoint,
148) -> Scalar {
149    let mut hasher = Sha256::new();
150    hasher.update(b"nizk-equality");
151    hasher.update(y1.to_sec1_point(true).as_bytes());
152    hasher.update(y2.to_sec1_point(true).as_bytes());
153    hasher.update(c1.to_sec1_point(true).as_bytes());
154    hasher.update(c2.to_sec1_point(true).as_bytes());
155    let fb: [u8; 32] = hasher.finalize().into();
156    hash_to_scalar(fb)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use p256::elliptic_curve::Field;
163
164    #[test]
165    fn dlog_proof_verifies() {
166        let secret = Scalar::random(&mut UnwrapErr(SysRng));
167        let proof = prove_dlog(&secret);
168        let public = (ProjectivePoint::GENERATOR * secret).to_affine();
169        assert!(verify_dlog(&public, &proof));
170    }
171
172    #[test]
173    fn dlog_wrong_public_rejected() {
174        let secret = Scalar::random(&mut UnwrapErr(SysRng));
175        let proof = prove_dlog(&secret);
176        let wrong =
177            (ProjectivePoint::GENERATOR * Scalar::random(&mut UnwrapErr(SysRng))).to_affine();
178        assert!(!verify_dlog(&wrong, &proof));
179    }
180
181    #[test]
182    fn dlog_tampered_response_rejected() {
183        let secret = Scalar::random(&mut UnwrapErr(SysRng));
184        let mut proof = prove_dlog(&secret);
185        let public = (ProjectivePoint::GENERATOR * secret).to_affine();
186        proof.response += Scalar::ONE;
187        assert!(!verify_dlog(&public, &proof));
188    }
189
190    #[test]
191    fn dlog_proof_non_deterministic() {
192        let secret = Scalar::random(&mut UnwrapErr(SysRng));
193        let p1 = prove_dlog(&secret);
194        let p2 = prove_dlog(&secret);
195        assert_ne!(p1.commitment, p2.commitment);
196    }
197
198    #[test]
199    fn equality_proof_verifies() {
200        let secret = Scalar::random(&mut UnwrapErr(SysRng));
201        let g1 = AffinePoint::GENERATOR;
202        let g2 = (ProjectivePoint::GENERATOR * Scalar::from(2u32)).to_affine();
203
204        let (p1, p2) = prove_dlog_equality(&secret, &g1, &g2);
205        let y1 = (ProjectivePoint::from(g1) * secret).to_affine();
206        let y2 = (ProjectivePoint::from(g2) * secret).to_affine();
207
208        assert!(verify_dlog_equality(&y1, &y2, &g1, &g2, &p1, &p2));
209    }
210
211    #[test]
212    fn equality_wrong_secret_rejected() {
213        let secret = Scalar::random(&mut UnwrapErr(SysRng));
214        let g1 = AffinePoint::GENERATOR;
215        let g2 = (ProjectivePoint::GENERATOR * Scalar::from(2u32)).to_affine();
216
217        let (p1, p2) = prove_dlog_equality(&secret, &g1, &g2);
218        let wrong = Scalar::random(&mut UnwrapErr(SysRng));
219        let y1 = (ProjectivePoint::from(g1) * wrong).to_affine();
220        let y2 = (ProjectivePoint::from(g2) * secret).to_affine();
221
222        assert!(!verify_dlog_equality(&y1, &y2, &g1, &g2, &p1, &p2));
223    }
224
225    #[test]
226    fn nizk_proof_serializes() {
227        let secret = Scalar::random(&mut UnwrapErr(SysRng));
228        let proof = prove_dlog(&secret);
229        // Just verify it has the right structure
230        assert!(proof.challenge != Scalar::ZERO || proof.response != Scalar::ZERO);
231    }
232}
233
234#[cfg(test)]
235mod adversarial_tests {
236    //! Every verify() carries a paired rejects-forgery test: a
237    //! self-consistent but false statement must never verify.
238
239    use super::*;
240    use p256::elliptic_curve::Field as _;
241
242    fn keypair() -> (Scalar, AffinePoint) {
243        let sk = Scalar::random(&mut UnwrapErr(SysRng));
244        let pk = (ProjectivePoint::GENERATOR * sk).to_affine();
245        (sk, pk)
246    }
247
248    #[test]
249    fn rejects_forged_challenge_dlog() {
250        let (sk, pk) = keypair();
251        let proof = prove_dlog(&sk);
252        let mut forged = proof.clone();
253        forged.challenge += Scalar::ONE; // breaks the transcript hash
254        assert!(!verify_dlog(&pk, &forged));
255    }
256
257    #[test]
258    fn rejects_proof_for_a_different_key() {
259        let (sk, _) = keypair();
260        let (_, other_pk) = keypair();
261        let proof = prove_dlog(&sk);
262        // Valid transcript bound to the wrong statement.
263        assert!(!verify_dlog(&other_pk, &proof));
264    }
265
266    #[test]
267    fn rejects_tampered_response_dlog() {
268        let (sk, pk) = keypair();
269        let mut proof = prove_dlog(&sk);
270        proof.response += Scalar::ONE;
271        assert!(!verify_dlog(&pk, &proof));
272    }
273
274    #[test]
275    fn rejects_mismatched_equality_proofs() {
276        let (sk, _) = keypair();
277        let g1 = ProjectivePoint::GENERATOR.to_affine();
278        let g2 = (ProjectivePoint::GENERATOR * Scalar::from(7u64)).to_affine();
279        let (p1, mut p2) = prove_dlog_equality(&sk, &g1, &g2);
280        // Same challenge, but a response belonging to another secret.
281        p2.response += Scalar::ONE;
282        let y1 = (ProjectivePoint::from(g1) * sk).to_affine();
283        let y2 = (ProjectivePoint::from(g2) * sk).to_affine();
284        assert!(!verify_dlog_equality(&y1, &y2, &g1, &g2, &p1, &p2));
285    }
286}