Skip to main content

curvy_core/
eddsa.rs

1//! EdDSA-Poseidon over BabyJubjub - a faithful port of `@zk-kit/eddsa-poseidon`'s
2//! default (BLAKE-1 / original BLAKE-512) entry, exposed here as
3//! `pub_from_private_key_hex`, `ephemeral_pub_key` and `sign_hex`.
4//!
5//! Parity hazards baked in here (each diverges from circomlibjs):
6//! - the private key is hashed with **original BLAKE-512** (see [`crate::blake512`]);
7//! - `signMessage` computes `S = r + hm·s mod l` with the **un-shifted** pruned
8//!   scalar `s` (not `s >> 3`). Because `pruneBuffer` zeroes the low 3 bits,
9//!   `s = 8·(s>>3)`, so it still verifies - but the `S` *value* differs from
10//!   circomlibjs by a factor of 8. We must match `@zk-kit`, which the on-chain
11//!   `EdDSAPoseidonVerifier` checks.
12
13use std::{fmt, sync::LazyLock};
14
15use hmac::{Hmac, KeyInit, Mac};
16use num_bigint::BigUint;
17use sha2::Sha512;
18
19use crate::babyjubjub::{
20    BASE8, BabyJubError, BabyJubPoint, BabyJubScalar, BabyJubSecretScalar, Point, SUB_ORDER,
21    add_point, mul_point_escalar, public_key_from_scalar,
22};
23use crate::blake512::blake512;
24use crate::encoding::{biguint_to_le_bytes, from_hex, le_bytes_to_biguint};
25use crate::field::{Bn254Fr, fr_from_biguint, fr_to_biguint};
26use crate::poseidon::poseidon;
27
28const SCALAR_NONCE_LABEL: &[u8] = b"CURVY_BABYJUB_SCALAR_NONCE_V1";
29
30static NONCE_REJECTION_LIMIT: LazyLock<BigUint> = LazyLock::new(|| {
31    let two_512 = BigUint::from(1u8) << 512usize;
32    &two_512 - (&two_512 % &*SUB_ORDER)
33});
34
35type HmacSha512 = Hmac<Sha512>;
36
37/// EdDSA-Poseidon signature: the point `R8` and the scalar `S` (`S < l`).
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Signature {
40    pub r8: Point,
41    pub s: BigUint,
42}
43
44/// Direct-scalar signature with checked subgroup points and a canonical response
45/// scalar.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ScalarSignature {
48    pub r8: BabyJubPoint,
49    pub s: BabyJubScalar,
50}
51
52impl ScalarSignature {
53    /// Convert to the established witness signature shape without changing values.
54    pub fn to_signature(&self) -> Signature {
55        Signature {
56            r8: self.r8.as_tuple(),
57            s: self.s.as_biguint().clone(),
58        }
59    }
60}
61
62/// An owned scalar-native signing key. Its public point is derived directly from
63/// the scalar; seed hashing, pruning, and clamping are never invoked.
64pub struct ScalarSigningKey {
65    secret: BabyJubSecretScalar,
66    public: BabyJubPoint,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum ScalarSignatureError {
71    InvalidKey(BabyJubError),
72    NonceCounterExhausted,
73    InternalVerificationFailed,
74}
75
76impl fmt::Display for ScalarSignatureError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::InvalidKey(e) => write!(f, "invalid scalar signing key: {e}"),
80            Self::NonceCounterExhausted => f.write_str("deterministic nonce counter exhausted"),
81            Self::InternalVerificationFailed => {
82                f.write_str("scalar signature failed internal verification")
83            }
84        }
85    }
86}
87
88impl std::error::Error for ScalarSignatureError {}
89
90impl From<BabyJubError> for ScalarSignatureError {
91    fn from(value: BabyJubError) -> Self {
92        Self::InvalidKey(value)
93    }
94}
95
96impl ScalarSigningKey {
97    pub fn from_secret(secret: BabyJubSecretScalar) -> Self {
98        let public = public_key_from_scalar(&secret);
99        Self { secret, public }
100    }
101
102    pub fn from_decimal(value: &str) -> Result<Self, ScalarSignatureError> {
103        Ok(Self::from_secret(BabyJubSecretScalar::try_from_dec(value)?))
104    }
105
106    pub fn from_le_bytes(bytes: [u8; 32]) -> Result<Self, ScalarSignatureError> {
107        Ok(Self::from_secret(BabyJubSecretScalar::try_from_le_bytes(
108            bytes,
109        )?))
110    }
111
112    #[inline]
113    pub fn verifying_key(&self) -> &BabyJubPoint {
114        &self.public
115    }
116
117    pub fn sign_curvy_v1(&self, message: Bn254Fr) -> Result<ScalarSignature, ScalarSignatureError> {
118        sign_scalar_compat(message, &self.secret, &self.public)
119    }
120}
121
122/// `pruneBuffer`: clear the low 3 bits and force the top two bits of a 32-byte
123/// little-endian scalar buffer (BabyJubjub key clamping).
124fn prune_buffer(mut b: [u8; 32]) -> [u8; 32] {
125    b[0] &= 0xf8;
126    b[31] &= 0x7f;
127    b[31] |= 0x40;
128    b
129}
130
131/// First 32 bytes of `BLAKE-512(private_key)`, pruned.
132fn pruned_scalar_buffer(private_key: &[u8]) -> [u8; 32] {
133    let hash = blake512(private_key);
134    let mut h32 = [0u8; 32];
135    h32.copy_from_slice(&hash[0..32]);
136    prune_buffer(h32)
137}
138
139/// `deriveSecretScalar` - `(LE(pruned) >> 3) mod l`.
140pub fn derive_secret_scalar(private_key: &[u8]) -> BigUint {
141    let pruned = pruned_scalar_buffer(private_key);
142    (le_bytes_to_biguint(&pruned) >> 3u32) % &*SUB_ORDER
143}
144
145/// `derivePublicKey` - `deriveSecretScalar(pk) · Base8`.
146pub fn derive_public_key(private_key: &[u8]) -> Point {
147    mul_point_escalar(*BASE8, &derive_secret_scalar(private_key))
148}
149
150/// `pubFromPrivateKey(hex)` - public key from a hex private key
151/// (`Buffer.from(hex, "hex")` semantics: the hex is decoded to raw bytes first).
152pub fn pub_from_private_key_hex(hex: &str) -> Point {
153    derive_public_key(&from_hex(hex))
154}
155
156/// `ephemeralPubKey(scalar)` - `R = scalar · Base8`.
157pub fn ephemeral_pub_key(scalar: &BigUint) -> Point {
158    mul_point_escalar(*BASE8, scalar)
159}
160
161/// `signMessage(private_key, message)` - EdDSA-Poseidon over BabyJubjub.
162///
163/// `message` is a **raw integer**, not a field element: the TS does not reduce it
164/// before packing its little-endian bytes into the `r` derivation, so a message in
165/// `[modulus, 2^256)` produces a different `R8`/`S` than its reduced value would.
166/// The Poseidon input `hm`, by contrast, reduces `message` (Poseidon reduces all
167/// inputs internally). Panics on a message `>= 2^256` (matches the TS 32-byte guard).
168pub fn sign(message: &BigUint, private_key: &[u8]) -> Signature {
169    let hash = blake512(private_key);
170
171    let mut h32 = [0u8; 32];
172    h32.copy_from_slice(&hash[0..32]);
173    let s = le_bytes_to_biguint(&prune_buffer(h32)); // un-shifted pruned scalar
174    let a = mul_point_escalar(*BASE8, &(&s >> 3u32));
175
176    // r = LE(BLAKE-512(hash[32..64] || LE32(message))) mod l  - message un-reduced.
177    let msg_buff = biguint_to_le_bytes(message, 32);
178    let mut compose = Vec::with_capacity(64);
179    compose.extend_from_slice(&hash[32..64]);
180    compose.extend_from_slice(&msg_buff);
181    let r = le_bytes_to_biguint(&blake512(&compose)) % &*SUB_ORDER;
182
183    let r8 = mul_point_escalar(*BASE8, &r);
184    let hm = poseidon(&[r8.0, r8.1, a.0, a.1, fr_from_biguint(message)]);
185
186    // S = (r + hm·s) mod l  - s un-shifted (see module note).
187    let s_sig = (r + fr_to_biguint(&hm) * s) % &*SUB_ORDER;
188
189    Signature { r8, s: s_sig }
190}
191
192/// `sign(message, privateKeyHex)` - the hex-keyed signing entry point.
193pub fn sign_hex(message: &BigUint, hex: &str) -> Signature {
194    sign(message, &from_hex(hex))
195}
196
197fn deterministic_scalar_nonce(
198    secret: &BabyJubSecretScalar,
199    public: &BabyJubPoint,
200    message: Bn254Fr,
201) -> Result<BabyJubSecretScalar, ScalarSignatureError> {
202    let key = secret.to_le_32();
203    let ax = Bn254Fr::from_fr(public.x()).to_le_32();
204    let ay = Bn254Fr::from_fr(public.y()).to_le_32();
205    let msg = message.to_le_32();
206
207    for counter in 0..=u32::MAX {
208        let mut mac = HmacSha512::new_from_slice(&key).expect("HMAC accepts a 32-byte key");
209        mac.update(SCALAR_NONCE_LABEL);
210        mac.update(&ax);
211        mac.update(&ay);
212        mac.update(&msg);
213        mac.update(&counter.to_be_bytes());
214        let digest = mac.finalize().into_bytes();
215        let candidate = BigUint::from_bytes_le(&digest);
216        if candidate >= *NONCE_REJECTION_LIMIT {
217            continue;
218        }
219        let reduced = candidate % &*SUB_ORDER;
220        if reduced != BigUint::from(0u8) {
221            return Ok(BabyJubSecretScalar::try_from_biguint(reduced)
222                .expect("nonce is canonical and non-zero"));
223        }
224    }
225    Err(ScalarSignatureError::NonceCounterExhausted)
226}
227
228/// Sign a canonical Curvy field message directly with a BabyJubJub subgroup
229/// scalar. This is compatible with the deployed circomlib equation:
230///
231/// `S*Base8 = R8 + Poseidon(R8,A,M)*8*A`.
232pub fn sign_scalar_compat(
233    message: Bn254Fr,
234    secret: &BabyJubSecretScalar,
235    public: &BabyJubPoint,
236) -> Result<ScalarSignature, ScalarSignatureError> {
237    let expected_public = public_key_from_scalar(secret);
238    if &expected_public != public {
239        return Err(ScalarSignatureError::InternalVerificationFailed);
240    }
241
242    let nonce = deterministic_scalar_nonce(secret, public, message)?;
243    let r = nonce.to_biguint();
244    let r8 = public_key_from_scalar(&nonce);
245    let h = poseidon(&[r8.x(), r8.y(), public.x(), public.y(), message.into_inner()]);
246    let e = (BigUint::from(8u8) * fr_to_biguint(&h)) % &*SUB_ORDER;
247    let response = (r + e * secret.to_biguint()) % &*SUB_ORDER;
248    let signature = ScalarSignature {
249        r8,
250        s: BabyJubScalar::try_from_biguint(response)
251            .expect("response was reduced modulo subgroup order"),
252    };
253    if !verify_scalar_compat(message, public, &signature) {
254        return Err(ScalarSignatureError::InternalVerificationFailed);
255    }
256    Ok(signature)
257}
258
259/// Verify the checked scalar-native signature using the exact equation enforced
260/// by Curvy's current `EdDSAPoseidonVerifier`.
261pub fn verify_scalar_compat(
262    message: Bn254Fr,
263    public: &BabyJubPoint,
264    signature: &ScalarSignature,
265) -> bool {
266    if public.is_identity() || signature.r8.is_identity() {
267        return false;
268    }
269    let h = poseidon(&[
270        signature.r8.x(),
271        signature.r8.y(),
272        public.x(),
273        public.y(),
274        message.into_inner(),
275    ]);
276    let e = (BigUint::from(8u8) * fr_to_biguint(&h)) % &*SUB_ORDER;
277    let left = mul_point_escalar(*BASE8, signature.s.as_biguint());
278    let right = add_point(
279        signature.r8.as_tuple(),
280        mul_point_escalar(public.as_tuple(), &e),
281    );
282    left == right
283}