iris_crypto/
cheetah.rs

1use ibig::UBig;
2use iris_ztd::{
3    crypto::cheetah::{
4        ch_add, ch_neg, ch_scal_big, trunc_g_order, CheetahPoint, F6lt, A_GEN, G_ORDER,
5    },
6    tip5::hash::hash_varlen,
7    Belt, Digest, Hashable, Noun, NounDecode, NounEncode,
8};
9use iris_ztd_derive::{NounDecode, NounEncode};
10extern crate alloc;
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, NounEncode, NounDecode)]
13pub struct PublicKey(pub CheetahPoint);
14
15impl PublicKey {
16    pub fn verify(&self, m: &Digest, sig: &Signature) -> bool {
17        if sig.c == UBig::from(0u64)
18            || sig.c >= *G_ORDER
19            || sig.s == UBig::from(0u64)
20            || sig.s >= *G_ORDER
21        {
22            return false;
23        }
24
25        // Compute scalar = s*G - c*pubkey
26        // This is equivalent to: scalar = s*G + (-c)*pubkey
27        let sg = match ch_scal_big(&sig.s, &A_GEN) {
28            Ok(pt) => pt,
29            Err(_) => return false,
30        };
31        let c_pk = match ch_scal_big(&sig.c, &self.0) {
32            Ok(pt) => pt,
33            Err(_) => return false,
34        };
35        let scalar = match ch_add(&sg, &ch_neg(&c_pk)) {
36            Ok(pt) => pt,
37            Err(_) => return false,
38        };
39        let chal = {
40            let mut transcript: Vec<Belt> = Vec::new();
41            transcript.extend_from_slice(&scalar.x.0);
42            transcript.extend_from_slice(&scalar.y.0);
43            transcript.extend_from_slice(&self.0.x.0);
44            transcript.extend_from_slice(&self.0.y.0);
45            transcript.extend_from_slice(&m.0);
46            trunc_g_order(&hash_varlen(&mut transcript))
47        };
48
49        chal == sig.c
50    }
51
52    pub fn to_be_bytes(&self) -> [u8; 97] {
53        let mut data = [0u8; 97];
54        data[0] = 0x01; // prefix byte
55        let mut offset = 1;
56        // y-coordinate: 6 belts × 8 bytes = 48 bytes
57        for belt in self.0.y.0.iter().rev() {
58            data[offset..offset + 8].copy_from_slice(&belt.0.to_be_bytes());
59            offset += 8;
60        }
61        // x-coordinate: 6 belts × 8 bytes = 48 bytes
62        for belt in self.0.x.0.iter().rev() {
63            data[offset..offset + 8].copy_from_slice(&belt.0.to_be_bytes());
64            offset += 8;
65        }
66        data
67    }
68
69    pub fn from_be_bytes(bytes: &[u8]) -> PublicKey {
70        let mut x = [Belt(0); 6];
71        let mut y = [Belt(0); 6];
72
73        // y-coordinate: bytes 1-48
74        for i in 0..6 {
75            let offset = 1 + i * 8;
76            let mut buf = [0u8; 8];
77            buf.copy_from_slice(&bytes[offset..offset + 8]);
78            y[5 - i] = Belt(u64::from_be_bytes(buf));
79        }
80
81        // x-coordinate: bytes 49-96
82        for i in 0..6 {
83            let offset = 49 + i * 8;
84            let mut buf = [0u8; 8];
85            buf.copy_from_slice(&bytes[offset..offset + 8]);
86            x[5 - i] = Belt(u64::from_be_bytes(buf));
87        }
88
89        PublicKey(CheetahPoint {
90            x: F6lt(x),
91            y: F6lt(y),
92            inf: false,
93        })
94    }
95
96    /// SLIP-10 compatible serialization (legacy 65-byte format for compatibility)
97    pub(crate) fn to_slip10_bytes(&self) -> Vec<u8> {
98        let mut data = Vec::new();
99        for belt in self.0.y.0.iter().rev().chain(self.0.x.0.iter().rev()) {
100            data.extend_from_slice(&belt.0.to_be_bytes());
101        }
102        data
103    }
104}
105
106impl Hashable for PublicKey {
107    fn hash(&self) -> Digest {
108        self.to_noun().hash()
109    }
110}
111
112#[derive(Debug, Clone)]
113pub struct Signature {
114    pub c: UBig, // challenge
115    pub s: UBig, // signature scalar
116}
117
118impl NounEncode for Signature {
119    fn to_noun(&self) -> Noun {
120        (
121            Belt::from_bytes(&self.c.to_le_bytes()).as_slice(),
122            Belt::from_bytes(&self.s.to_le_bytes()).as_slice(),
123        )
124            .to_noun()
125    }
126}
127
128impl NounDecode for Signature {
129    fn from_noun(noun: &Noun) -> Option<Self> {
130        let (c, s): (Vec<Belt>, Vec<Belt>) = NounDecode::from_noun(noun)?;
131
132        let c = Belt::to_bytes(&c);
133        let s = Belt::to_bytes(&s);
134
135        Some(Signature {
136            c: UBig::from_le_bytes(&c),
137            s: UBig::from_le_bytes(&s),
138        })
139    }
140}
141
142impl Hashable for Signature {
143    fn hash(&self) -> Digest {
144        self.to_noun().hash()
145    }
146}
147
148#[derive(Debug, Clone)]
149pub struct PrivateKey(pub UBig);
150
151impl PrivateKey {
152    pub fn public_key(&self) -> PublicKey {
153        PublicKey(ch_scal_big(&self.0, &A_GEN).unwrap())
154    }
155
156    pub fn sign(&self, m: &Digest) -> Signature {
157        let pubkey = self.public_key().0;
158        let nonce = {
159            let mut transcript = Vec::new();
160            transcript.extend_from_slice(&pubkey.x.0);
161            transcript.extend_from_slice(&pubkey.y.0);
162            transcript.extend_from_slice(&m.0);
163            self.0.to_le_bytes().chunks(4).for_each(|chunk| {
164                let mut buf = [0u8; 4];
165                buf[..chunk.len()].copy_from_slice(chunk);
166                transcript.push(Belt(u32::from_le_bytes(buf) as u64));
167            });
168            trunc_g_order(&hash_varlen(&mut transcript))
169        };
170        let chal = {
171            // scalar = nonce * G
172            let scalar = ch_scal_big(&nonce, &A_GEN).unwrap();
173            let mut transcript = Vec::new();
174            transcript.extend_from_slice(&scalar.x.0);
175            transcript.extend_from_slice(&scalar.y.0);
176            transcript.extend_from_slice(&pubkey.x.0);
177            transcript.extend_from_slice(&pubkey.y.0);
178            transcript.extend_from_slice(&m.0);
179            trunc_g_order(&hash_varlen(&mut transcript))
180        };
181        let sig = (&nonce + &chal * &self.0) % &*G_ORDER;
182        Signature { c: chal, s: sig }
183    }
184
185    pub fn to_be_bytes(&self) -> [u8; 32] {
186        let bytes = self.0.to_be_bytes();
187        let mut arr = [0u8; 32];
188        arr[32 - bytes.len()..].copy_from_slice(&bytes);
189        arr
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_sign_and_verify() {
199        let priv_key = PrivateKey(UBig::from(123u64));
200        let digest = Digest([Belt(1), Belt(2), Belt(3), Belt(4), Belt(5)]);
201        let signature = priv_key.sign(&digest);
202        let pubkey = priv_key.public_key();
203        assert!(
204            pubkey.verify(&digest, &signature),
205            "Signature verification failed!"
206        );
207
208        // Corrupting digest, signature, or pubkey should all cause failure
209        let mut wrong_digest = digest;
210        wrong_digest.0[0] = Belt(0);
211        assert!(
212            !pubkey.verify(&wrong_digest, &signature),
213            "Should reject wrong digest"
214        );
215        let mut wrong_sig = signature.clone();
216        wrong_sig.s += UBig::from(1u64);
217        assert!(
218            !pubkey.verify(&digest, &wrong_sig),
219            "Should reject wrong signature"
220        );
221        let mut wrong_pubkey = pubkey.clone();
222        wrong_pubkey.0.x.0[0].0 += 1;
223        assert!(
224            !wrong_pubkey.verify(&digest, &signature),
225            "Should reject wrong public key"
226        );
227    }
228
229    #[test]
230    fn test_vector() {
231        // from nockchain zkvm-jetpack cheetah_jets.rs test_batch_verify_affine
232        let digest = Digest([Belt(8), Belt(9), Belt(10), Belt(11), Belt(12)]);
233        let pubkey = PublicKey(CheetahPoint {
234            x: F6lt([
235                Belt(2754611494552410273),
236                Belt(8599518745794843693),
237                Belt(10526511002404673680),
238                Belt(4830863958577994148),
239                Belt(375185138577093320),
240                Belt(12938930721685970739),
241            ]),
242            y: F6lt([
243                Belt(3062714866612034253),
244                Belt(15671931273416742386),
245                Belt(4071440668668521568),
246                Belt(7738250649524482367),
247                Belt(5259065445844042557),
248                Belt(8456011930642078370),
249            ]),
250            inf: false,
251        });
252        let c_hex = "6f3cd43cd8709f4368aed04cd84292ab1c380cb645aaa7d010669d70375cbe88";
253        let s_hex = "5197ab182e307a350b5cf3606d6e99a6f35b0d382c8330dde6e51fb6ef8ebb8c";
254        let signature = Signature {
255            c: UBig::from_str_radix(c_hex, 16).unwrap(),
256            s: UBig::from_str_radix(s_hex, 16).unwrap(),
257        };
258        assert!(pubkey.verify(&digest, &signature));
259    }
260}