Skip to main content

ling_crypto/
zkp.rs

1//! Schnorr zero-knowledge proof of knowledge.
2//!
3//! Proves knowledge of a discrete log `x` s.t. `X = x·G`
4//! on the Ristretto255 group, without revealing `x`.
5//! Uses Fiat-Shamir transform (non-interactive via BLAKE3).
6
7use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT as G, RistrettoPoint, Scalar};
8use rand::rngs::OsRng;
9use zeroize::Zeroizing;
10
11pub struct SchnorrKeypair {
12    secret: Zeroizing<Scalar>,
13    pub public: RistrettoPoint,
14}
15
16impl SchnorrKeypair {
17    pub fn generate() -> Self {
18        let secret = Scalar::random(&mut OsRng);
19        let public = secret * G;
20        Self { secret: Zeroizing::new(secret), public }
21    }
22
23    pub fn from_scalar_bytes(bytes: [u8; 32]) -> Option<Self> {
24        let secret = Scalar::from_canonical_bytes(bytes).into_option()?;
25        let public = secret * G;
26        Some(Self { secret: Zeroizing::new(secret), public })
27    }
28
29    pub fn public_bytes(&self) -> [u8; 32] {
30        self.public.compress().to_bytes()
31    }
32
33    /// Create a proof of knowledge of the secret key, bound to `msg`.
34    pub fn prove(&self, msg: &[u8]) -> SchnorrProof {
35        let r = Scalar::random(&mut OsRng);
36        let r_point = r * G;
37        let c = challenge(&self.public, &r_point, msg);
38        let s = r + c * *self.secret;
39        SchnorrProof {
40            r_bytes: r_point.compress().to_bytes(),
41            s_bytes: s.to_bytes(),
42        }
43    }
44}
45
46/// Non-interactive Schnorr proof: (R, s) where s = r + c·x, c = H(X||R||msg).
47#[derive(Clone, Debug)]
48pub struct SchnorrProof {
49    pub r_bytes: [u8; 32], // commitment R = r·G
50    pub s_bytes: [u8; 32], // response s
51}
52
53/// Verify a Schnorr proof against a public key and message.
54pub fn schnorr_verify(pubkey_bytes: &[u8; 32], msg: &[u8], proof: &SchnorrProof) -> bool {
55    use curve25519_dalek::ristretto::CompressedRistretto;
56    let x_point = match CompressedRistretto(*pubkey_bytes).decompress() {
57        Some(p) => p,
58        None => return false,
59    };
60    let r_point = match CompressedRistretto(proof.r_bytes).decompress() {
61        Some(p) => p,
62        None => return false,
63    };
64    let s = match Scalar::from_canonical_bytes(proof.s_bytes).into_option() {
65        Some(s) => s,
66        None => return false,
67    };
68    let c = challenge(&x_point, &r_point, msg);
69    // Check: s·G == R + c·X
70    s * G == r_point + c * x_point
71}
72
73fn challenge(x_point: &RistrettoPoint, r_point: &RistrettoPoint, msg: &[u8]) -> Scalar {
74    let mut data = b"ling-schnorr-v1:".to_vec();
75    data.extend_from_slice(&x_point.compress().to_bytes());
76    data.extend_from_slice(&r_point.compress().to_bytes());
77    data.extend_from_slice(msg);
78    let h = blake3::hash(&data);
79    Scalar::from_bytes_mod_order(*h.as_bytes())
80}