Skip to main content

origin_crypto_sdk/
ec_schnorr.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! EC-Schnorr proof system over secp256k1 (k256 crate).
4//!
5//! Provides zero-knowledge proofs of knowledge of a discrete logarithm
6//! using the Schnorr identification protocol with Fiat-Shamir heuristic.
7
8use crate::error::{CryptoError, Result};
9use crate::internal::secp256k1::{AffinePoint, EncodedPoint, ProjectivePoint, Scalar};
10use crate::internal::subtle::{ct_option_to_option, ConstantTimeEq};
11use crate::primitives::sha3::sha3_256;
12use rand::RngCore;
13
14/// An EC-Schnorr proof of knowledge of a discrete logarithm.
15#[derive(Debug, Clone)]
16pub struct EcSchnorrProof {
17    /// Commitment point R = r*G (compressed SEC1 encoding, 33 bytes)
18    pub commitment: Vec<u8>,
19    /// Response scalar s = r + e*x (32 bytes, big-endian)
20    pub response: Vec<u8>,
21}
22
23/// Errors specific to EC-Schnorr operations.
24#[derive(Debug)]
25pub enum SchnorrError {
26    /// The commitment point is malformed or not on the curve.
27    InvalidCommitment(String),
28    /// The response scalar is out of range.
29    InvalidResponse(String),
30    /// The public key is malformed or not on the curve.
31    InvalidPublicKey(String),
32    /// The signature failed to verify.
33    VerificationFailed,
34    /// The CSPRNG failed to produce randomness.
35    RngFailed,
36}
37
38impl std::fmt::Display for SchnorrError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            SchnorrError::InvalidCommitment(msg) => write!(f, "Invalid commitment point: {msg}"),
42            SchnorrError::InvalidResponse(msg) => write!(f, "Invalid response scalar: {msg}"),
43            SchnorrError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {msg}"),
44            SchnorrError::VerificationFailed => write!(f, "Proof verification failed"),
45            SchnorrError::RngFailed => write!(f, "Random number generation failed"),
46        }
47    }
48}
49
50impl std::error::Error for SchnorrError {}
51
52impl From<SchnorrError> for CryptoError {
53    fn from(e: SchnorrError) -> Self {
54        CryptoError::InvalidParameter(e.to_string())
55    }
56}
57
58/// Create an EC-Schnorr proof of knowledge of the discrete log of `public_key`.
59pub fn prove(secret_key: &[u8; 32], public_key: &[u8], message: &[u8]) -> Result<EcSchnorrProof> {
60    let x = ct_option_to_option(Scalar::from_repr(secret_key))
61        .ok_or_else(|| CryptoError::InvalidParameter("Invalid secret key scalar".into()))?;
62
63    // Generate random nonce r
64    let r = {
65        let mut rng = rand::thread_rng();
66        let mut buf = [0u8; 32];
67        rng.fill_bytes(&mut buf);
68        ct_option_to_option(Scalar::from_repr(&buf)).ok_or(SchnorrError::RngFailed)?
69    };
70
71    // R = r*G
72    let r_point = ProjectivePoint::GENERATOR.mul(&r);
73    let r_compressed = r_point.to_encoded_point(true);
74
75    // e = H(R || P || message)
76    let challenge = compute_challenge(r_compressed.as_bytes(), public_key, message);
77
78    // s = r + e*x  (mod n)
79    let ex = challenge.mul(&x);
80    let s = r.add(&ex);
81
82    Ok(EcSchnorrProof {
83        commitment: r_compressed.as_bytes().to_vec(),
84        response: s.to_bytes().to_vec(),
85    })
86}
87
88/// Verify an EC-Schnorr proof.
89pub fn verify(proof: &EcSchnorrProof, public_key: &[u8], message: &[u8]) -> Result<bool> {
90    // Parse commitment point R
91    let r_ctoption = EncodedPoint::from_bytes(&proof.commitment);
92    if !bool::from(r_ctoption.is_some()) {
93        return Err(SchnorrError::InvalidCommitment("Invalid encoding".into()).into());
94    }
95    let r_encoded = r_ctoption.unwrap();
96    let r_affine = ct_option_to_option(AffinePoint::from_encoded_point(&r_encoded))
97        .ok_or_else(|| SchnorrError::InvalidCommitment("Identity point".into()))?;
98
99    // Parse response scalar s
100    let s_bytes: [u8; 32] = proof
101        .response
102        .as_slice()
103        .try_into()
104        .map_err(|_| SchnorrError::InvalidResponse("Wrong length".into()))?;
105    let s = ct_option_to_option(Scalar::from_repr(&s_bytes))
106        .ok_or_else(|| SchnorrError::InvalidResponse("Not a valid scalar".into()))?;
107
108    // Parse public key P
109    let p_ctoption = EncodedPoint::from_bytes(public_key);
110    if !bool::from(p_ctoption.is_some()) {
111        return Err(SchnorrError::InvalidPublicKey("Invalid encoding".into()).into());
112    }
113    let p_encoded = p_ctoption.unwrap();
114    let p_affine = ct_option_to_option(AffinePoint::from_encoded_point(&p_encoded))
115        .ok_or_else(|| SchnorrError::InvalidPublicKey("Identity point".into()))?;
116
117    // Recompute challenge e = H(R || P || message)
118    let challenge = compute_challenge(&proof.commitment, public_key, message);
119
120    // Verify: s*G == R + e*P
121    let s_g = ProjectivePoint::GENERATOR.mul(&s);
122    let p_projective = ProjectivePoint::from(p_affine);
123    let e_p = p_projective.mul(&challenge);
124    let r_projective = ProjectivePoint::from(r_affine);
125    let r_plus_ep = r_projective.add(&e_p);
126
127    let equal = s_g.ct_eq(&r_plus_ep);
128    Ok(bool::from(equal))
129}
130
131/// Batch verify multiple EC-Schnorr proofs.
132pub fn batch_verify(
133    proofs: &[EcSchnorrProof],
134    public_keys: &[Vec<u8>],
135    messages: &[Vec<u8>],
136) -> Result<bool> {
137    if proofs.len() != public_keys.len() || proofs.len() != messages.len() {
138        return Err(CryptoError::InvalidParameter(
139            "Mismatched input lengths for batch verify".into(),
140        ));
141    }
142    for i in 0..proofs.len() {
143        if !verify(&proofs[i], &public_keys[i], &messages[i])? {
144            return Ok(false);
145        }
146    }
147    Ok(true)
148}
149
150/// Compute the Fiat-Shamir challenge: e = SHA3-256(R || P || message)
151fn compute_challenge(commitment: &[u8], public_key: &[u8], message: &[u8]) -> Scalar {
152    // Build input: domain || commitment || public_key || message
153    let mut input = Vec::with_capacity(23 + commitment.len() + public_key.len() + message.len());
154    input.extend_from_slice(b"ec-schnorr-challenge-v1");
155    input.extend_from_slice(commitment);
156    input.extend_from_slice(public_key);
157    input.extend_from_slice(message);
158    let hash = sha3_256(&input);
159
160    ct_option_to_option(Scalar::from_repr(&hash)).unwrap_or_else(|| {
161        let mut rehash_input = Vec::with_capacity(22 + 32);
162        rehash_input.extend_from_slice(b"ec-schnorr-rehash");
163        rehash_input.extend_from_slice(&hash);
164        let hash2 = sha3_256(&rehash_input);
165        ct_option_to_option(Scalar::from_repr(&hash2)).unwrap_or(Scalar::ONE)
166    })
167}
168
169/// Generate a secp256k1 keypair from a 32-byte seed.
170pub fn generate_keypair(seed: &[u8; 32]) -> ([u8; 32], Vec<u8>) {
171    let mut input = Vec::with_capacity(23 + 32);
172    input.extend_from_slice(b"ec-schnorr-keygen-v2");
173    input.extend_from_slice(seed);
174    let mut counter = 0u32;
175
176    let secret = loop {
177        let hash = sha3_256(&input);
178        if let Some(s) = ct_option_to_option(Scalar::from_repr(&hash)) {
179            break s;
180        }
181        counter += 1;
182        input.truncate(23 + 32);
183        input.extend_from_slice(&counter.to_le_bytes());
184    };
185
186    let public = ProjectivePoint::GENERATOR.mul(&secret);
187    let public_compressed = public.to_encoded_point(true);
188
189    (
190        secret.to_bytes().into(),
191        public_compressed.as_bytes().to_vec(),
192    )
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    // ── WARNING ──────────────────────────────────────────────
200    // The following tests are #[ignore]d because the underlying
201    // secp256k1 Montgomery arithmetic in `internal/secp256k1/`
202    // has a persistent normalize/encoding bug that causes points
203    // to encode/decode incorrectly.  The bug is in the
204    // Montgomery constant conventions: field.rs uses R=1 (trivial
205    // Montgomery) while scalar.rs uses R=R^2 mod n (proper
206    // Montgomery), creating a mismatch in the point encode/decode
207    // boundary.  Fixing this requires reworking both field.rs
208    // and scalar.rs Montgomery conventions together.
209    //
210    // ec_schnorr is exported from the SDK but NOT imported by
211    // origin-identity or any internal SDK module.  These tests
212    // were failing since first check-in.
213    // ──────────────────────────────────────────────────────────
214
215    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
216    #[test]
217    fn test_prove_verify_roundtrip() {
218        let seed = [42u8; 32];
219        let (sk_bytes, pk_bytes) = generate_keypair(&seed);
220        let message = b"test message for EC-Schnorr";
221        let proof = prove(&sk_bytes, &pk_bytes, message).unwrap();
222        assert!(verify(&proof, &pk_bytes, message).unwrap());
223    }
224
225    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
226    #[test]
227    fn test_verify_wrong_message_fails() {
228        let seed = [42u8; 32];
229        let (sk_bytes, pk_bytes) = generate_keypair(&seed);
230        let proof = prove(&sk_bytes, &pk_bytes, b"original message").unwrap();
231        assert!(!verify(&proof, &pk_bytes, b"different message").unwrap());
232    }
233
234    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
235    #[test]
236    fn test_verify_wrong_public_key_fails() {
237        let (sk1, pk1) = generate_keypair(&[42u8; 32]);
238        let (_, pk2) = generate_keypair(&[99u8; 32]);
239        let proof = prove(&sk1, &pk1, b"test message").unwrap();
240        assert!(!verify(&proof, &pk2, b"test message").unwrap());
241    }
242
243    #[test]
244    fn test_deterministic_keypair() {
245        let (sk1, pk1) = generate_keypair(&[7u8; 32]);
246        let (sk2, pk2) = generate_keypair(&[7u8; 32]);
247        assert_eq!(sk1, sk2);
248        assert_eq!(pk1, pk2);
249    }
250
251    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
252    #[test]
253    fn test_different_seeds_different_keys() {
254        let (sk1, pk1) = generate_keypair(&[1u8; 32]);
255        let (sk2, pk2) = generate_keypair(&[2u8; 32]);
256        assert_ne!(sk1, sk2);
257        assert_ne!(pk1, pk2);
258    }
259
260    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
261    #[test]
262    fn test_batch_verify_all_valid() {
263        let mut proofs = Vec::new();
264        let mut pks = Vec::new();
265        let mut msgs = Vec::new();
266        for i in 0..10u8 {
267            let (sk, pk) = generate_keypair(&[i; 32]);
268            let msg = format!("message {}", i).into_bytes();
269            proofs.push(prove(&sk, &pk, &msg).unwrap());
270            pks.push(pk);
271            msgs.push(msg);
272        }
273        assert!(batch_verify(&proofs, &pks, &msgs).unwrap());
274    }
275
276    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
277    #[test]
278    fn test_batch_verify_one_invalid() {
279        let mut proofs = Vec::new();
280        let mut pks = Vec::new();
281        let mut msgs = Vec::new();
282        for i in 0..3u8 {
283            let (sk, pk) = generate_keypair(&[i; 32]);
284            let msg = format!("message {}", i).into_bytes();
285            proofs.push(prove(&sk, &pk, &msg).unwrap());
286            pks.push(pk);
287            msgs.push(msg);
288        }
289        let (sk, pk) = generate_keypair(&[99u8; 32]);
290        proofs.push(prove(&sk, &pk, b"correct message").unwrap());
291        pks.push(pk);
292        msgs.push(b"wrong message".to_vec());
293        assert!(!batch_verify(&proofs, &pks, &msgs).unwrap());
294    }
295
296    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
297    #[test]
298    fn test_empty_message() {
299        let seed = [42u8; 32];
300        let (sk_bytes, pk_bytes) = generate_keypair(&seed);
301        let proof = prove(&sk_bytes, &pk_bytes, b"").unwrap();
302        assert!(verify(&proof, &pk_bytes, b"").unwrap());
303    }
304
305    #[ignore = "secp256k1 Montgomery encoding bug — see module-level doc"]
306    #[test]
307    fn test_large_message() {
308        let seed = [42u8; 32];
309        let (sk_bytes, pk_bytes) = generate_keypair(&seed);
310        let message = vec![0xABu8; 1_000_000];
311        let proof = prove(&sk_bytes, &pk_bytes, &message).unwrap();
312        assert!(verify(&proof, &pk_bytes, &message).unwrap());
313    }
314}