Skip to main content

confium_crypto_vss/
schnorr.rs

1//! Schnorr proof of knowledge — non-interactive ZK proof of discrete log.
2//!
3//! Proves knowledge of `x` such that `Y = g^x`, without revealing `x`.
4//! Uses the Fiat-Shamir heuristic: the challenge is derived from a
5//! hash of the protocol transcript, making the proof non-interactive.
6
7use getrandom::SysRng;
8use p256::elliptic_curve::PrimeField;
9use p256::elliptic_curve::rand_core::{Rng, UnwrapErr};
10use p256::elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
11use p256::{AffinePoint, ProjectivePoint, Scalar};
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15/// A Schnorr proof: (R, z) serialized as hex strings.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct SchnorrProof {
18    /// Commitment point R = g^k (SEC1 compressed, hex).
19    pub r_hex: String,
20    /// Response scalar z = k + c*x (32 bytes, hex).
21    pub z_hex: String,
22}
23
24/// Errors during proof operations.
25#[derive(Debug, thiserror::Error)]
26pub enum SchnorrError {
27    /// Point decoding failed.
28    #[error("point decoding failed")]
29    InvalidPoint,
30    /// Scalar decoding failed.
31    #[error("scalar decoding failed")]
32    InvalidScalar,
33    /// Hex decoding failed.
34    #[error("hex decoding failed: {0}")]
35    HexError(String),
36}
37
38/// Create a Schnorr proof of knowledge of the discrete log of `public`.
39pub fn prove(secret: &Scalar, public: &AffinePoint, message: &[u8]) -> SchnorrProof {
40    use p256::elliptic_curve::Field;
41    let mut k_bytes = [0u8; 32];
42    UnwrapErr(SysRng).fill_bytes(&mut k_bytes);
43    let k_fb = p256::FieldBytes::from(k_bytes);
44    let k_ct = Scalar::from_repr(k_fb);
45    let k = Option::<Scalar>::from(k_ct).unwrap_or_else(|| Scalar::random(&mut UnwrapErr(SysRng)));
46
47    let r_point = ProjectivePoint::GENERATOR * k;
48    let r_affine = r_point.to_affine();
49
50    let c = fiat_shamir_challenge(public, &r_affine, message);
51    let z = k + secret * &c;
52
53    let r_encoded = r_affine.to_sec1_point(true);
54    let z_bytes: [u8; 32] = z.to_repr().into();
55
56    SchnorrProof {
57        r_hex: hex::encode(r_encoded.as_bytes()),
58        z_hex: hex::encode(z_bytes),
59    }
60}
61
62/// Verify a Schnorr proof.
63pub fn verify(
64    proof: &SchnorrProof,
65    public: &AffinePoint,
66    message: &[u8],
67) -> Result<bool, SchnorrError> {
68    let r_bytes = hex::decode(&proof.r_hex).map_err(|e| SchnorrError::HexError(e.to_string()))?;
69    let r_point = decode_point(&r_bytes)?;
70    let z_bytes = hex::decode(&proof.z_hex).map_err(|e| SchnorrError::HexError(e.to_string()))?;
71    if z_bytes.len() != 32 {
72        return Err(SchnorrError::InvalidScalar);
73    }
74    let z_arr: [u8; 32] = z_bytes.as_slice().try_into().unwrap();
75    let z_scalar = decode_scalar(&z_arr)?;
76
77    let c = fiat_shamir_challenge(public, &r_point, message);
78
79    let lhs = ProjectivePoint::GENERATOR * z_scalar;
80    let rhs = ProjectivePoint::from(r_point) + ProjectivePoint::from(*public) * c;
81
82    Ok(lhs == rhs)
83}
84
85fn fiat_shamir_challenge(public: &AffinePoint, r: &AffinePoint, message: &[u8]) -> Scalar {
86    let public_encoded = public.to_sec1_point(true);
87    let r_encoded = r.to_sec1_point(true);
88
89    let mut hasher = Sha256::new();
90    hasher.update(b"confium-schnorr-v1");
91    hasher.update(public_encoded.as_bytes());
92    hasher.update(r_encoded.as_bytes());
93    hasher.update(message);
94    let hash = hasher.finalize();
95
96    let mut fb: [u8; 32] = hash.into();
97    // Rejection sampling with re-hash: a zero challenge must be
98    // impossible by construction, not a fallback.
99    loop {
100        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(p256::FieldBytes::from(fb))) {
101            return s;
102        }
103        let mut h = Sha256::new();
104        h.update(b"confium-scalar-reduce-v1");
105        h.update(fb);
106        fb = h.finalize().into();
107    }
108}
109
110fn decode_point(bytes: &[u8]) -> Result<AffinePoint, SchnorrError> {
111    let encoded = p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
112        .map_err(|_| SchnorrError::InvalidPoint)?;
113    Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&encoded))
114        .ok_or(SchnorrError::InvalidPoint)
115}
116
117fn decode_scalar(bytes: &[u8; 32]) -> Result<Scalar, SchnorrError> {
118    let fb = p256::FieldBytes::from(*bytes);
119    let ct = Scalar::from_repr(fb);
120    Option::<Scalar>::from(ct).ok_or(SchnorrError::InvalidScalar)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use p256::elliptic_curve::Field as _;
127
128    fn random_keypair() -> (Scalar, AffinePoint) {
129        let mut buf = [0u8; 32];
130        UnwrapErr(SysRng).fill_bytes(&mut buf);
131        let fb = p256::FieldBytes::from(buf);
132        let ct = Scalar::from_repr(fb);
133        let secret =
134            Option::<Scalar>::from(ct).unwrap_or_else(|| Scalar::random(&mut UnwrapErr(SysRng)));
135        let public = (ProjectivePoint::GENERATOR * secret).to_affine();
136        (secret, public)
137    }
138
139    #[test]
140    fn valid_proof_verifies() {
141        let (secret, public) = random_keypair();
142        let proof = prove(&secret, &public, b"test message");
143        assert!(verify(&proof, &public, b"test message").unwrap());
144    }
145
146    #[test]
147    fn wrong_public_key_rejected() {
148        let (secret, correct_public) = random_keypair();
149        let (_, wrong_public) = random_keypair();
150        let proof = prove(&secret, &correct_public, b"msg");
151        assert!(verify(&proof, &correct_public, b"msg").unwrap());
152        assert!(!verify(&proof, &wrong_public, b"msg").unwrap());
153    }
154
155    #[test]
156    fn different_messages_produce_different_proofs() {
157        let (secret, public) = random_keypair();
158        let p1 = prove(&secret, &public, b"message A");
159        let p2 = prove(&secret, &public, b"message B");
160        assert_ne!(p1.r_hex, p2.r_hex);
161    }
162
163    #[test]
164    fn tampered_z_rejected() {
165        let (secret, public) = random_keypair();
166        let mut proof = prove(&secret, &public, b"msg");
167        let mut z_bytes = hex::decode(&proof.z_hex).unwrap();
168        z_bytes[0] ^= 0xFF;
169        proof.z_hex = hex::encode(&z_bytes);
170        assert!(!verify(&proof, &public, b"msg").unwrap());
171    }
172
173    #[test]
174    fn proof_serializes() {
175        let (secret, public) = random_keypair();
176        let proof = prove(&secret, &public, b"msg");
177        let json = serde_json::to_string(&proof).unwrap();
178        let recovered: SchnorrProof = serde_json::from_str(&json).unwrap();
179        assert_eq!(proof, recovered);
180    }
181
182    #[test]
183    fn empty_message_works() {
184        let (secret, public) = random_keypair();
185        let proof = prove(&secret, &public, b"");
186        assert!(verify(&proof, &public, b"").unwrap());
187    }
188
189    #[test]
190    fn proof_is_non_deterministic() {
191        let (secret, public) = random_keypair();
192        let p1 = prove(&secret, &public, b"same message");
193        let p2 = prove(&secret, &public, b"same message");
194        assert_ne!(p1.r_hex, p2.r_hex);
195        assert!(verify(&p1, &public, b"same message").unwrap());
196        assert!(verify(&p2, &public, b"same message").unwrap());
197    }
198}
199
200#[cfg(test)]
201mod adversarial_tests {
202    //! Paired rejects-forgery tests for the Schnorr signature
203    //! interface.
204
205    use super::*;
206    use p256::elliptic_curve::Field as _;
207
208    fn keypair() -> (Scalar, AffinePoint) {
209        let sk = Scalar::random(&mut UnwrapErr(SysRng));
210        let pk = (ProjectivePoint::GENERATOR * sk).to_affine();
211        (sk, pk)
212    }
213
214    #[test]
215    fn rejects_signature_for_a_different_message() {
216        let (sk, pk) = keypair();
217        let sig = prove(&sk, &pk, b"original message");
218        // Valid signature, wrong statement.
219        assert!(!verify(&sig, &pk, b"other message").unwrap());
220    }
221
222    #[test]
223    fn rejects_tampered_signature() {
224        let (sk, pk) = keypair();
225        let sig = prove(&sk, &pk, b"message");
226        // Corrupt the scalar response.
227        let mut z = hex::decode(&sig.z_hex).unwrap();
228        z[0] ^= 0x01;
229        let mut forged = sig.clone();
230        forged.z_hex = z.iter().map(|b| format!("{b:02x}")).collect();
231        assert!(!verify(&forged, &pk, b"message").unwrap());
232    }
233
234    #[test]
235    fn rejects_signature_under_a_wrong_key() {
236        let (sk, _) = keypair();
237        let (_, other_pk) = keypair();
238        let sig = prove(&sk, &other_pk, b"message"); // prove against wrong pk
239        assert!(!verify(&sig, &other_pk, b"message").unwrap());
240    }
241}