starknet_core/
crypto.rs

1use starknet_types_core::felt::Felt;
2
3pub use starknet_crypto::{pedersen_hash, ExtendedSignature, Signature};
4use starknet_crypto::{rfc6979_generate_k, sign, verify, SignError, VerifyError};
5
6mod errors {
7    use core::fmt::{Display, Formatter, Result};
8
9    /// Errors when performing ECDSA [`sign`](fn.ecdsa_sign) operations.
10    #[derive(Debug)]
11    pub enum EcdsaSignError {
12        /// The message hash is not in the range of `[0, 2^251)`.
13        MessageHashOutOfRange,
14    }
15
16    #[derive(Debug)]
17    /// Errors when performing ECDSA [`verify`](fn.ecdsa_verify) operations.
18    pub enum EcdsaVerifyError {
19        /// The message hash is not in the range of `[0, 2^251)`.
20        MessageHashOutOfRange,
21        /// The public key is not a valid point on the STARK curve.
22        InvalidPublicKey,
23        /// The `r` value is not in the range of `[0, 2^251)`.
24        SignatureROutOfRange,
25        /// The `s` value is not in the range of `[0, 2^251)`.
26        SignatureSOutOfRange,
27    }
28
29    #[cfg(feature = "std")]
30    impl std::error::Error for EcdsaSignError {}
31
32    impl Display for EcdsaSignError {
33        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
34            match self {
35                Self::MessageHashOutOfRange => write!(f, "message hash out of range"),
36            }
37        }
38    }
39
40    #[cfg(feature = "std")]
41    impl std::error::Error for EcdsaVerifyError {}
42
43    impl Display for EcdsaVerifyError {
44        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
45            match self {
46                Self::MessageHashOutOfRange => write!(f, "message hash out of range"),
47                Self::InvalidPublicKey => write!(f, "invalid public key"),
48                Self::SignatureROutOfRange => write!(f, "signature r value out of range"),
49                Self::SignatureSOutOfRange => write!(f, "signature s value out of range"),
50            }
51        }
52    }
53}
54pub use errors::{EcdsaSignError, EcdsaVerifyError};
55
56/// Computes the Pedersen hash of a list of [`Felt`].
57///
58/// The hash is computed by starting with `0`, hashing it recursively against all elements in
59/// the list, and finally also hashing against the length of the list.
60///
61/// For example, calling `compute_hash_on_elements([7, 8])` would return:
62///
63/// ```markdown
64/// pedersen_hash(pedersen_hash(pedersen_hash(0, 7)), 8), 2)
65/// ```
66pub fn compute_hash_on_elements<'a, ESI, II>(data: II) -> Felt
67where
68    ESI: ExactSizeIterator<Item = &'a Felt>,
69    II: IntoIterator<IntoIter = ESI>,
70{
71    let mut current_hash = Felt::ZERO;
72    let data_iter = data.into_iter();
73    let data_len = Felt::from(data_iter.len());
74
75    for elem in data_iter {
76        current_hash = pedersen_hash(&current_hash, elem);
77    }
78
79    pedersen_hash(&current_hash, &data_len)
80}
81
82/// Signs a hash using deterministic ECDSA on the STARK curve. The signature returned can be used
83/// to recover the public key.
84pub fn ecdsa_sign(
85    private_key: &Felt,
86    message_hash: &Felt,
87) -> Result<ExtendedSignature, EcdsaSignError> {
88    // Seed-retry logic ported from `cairo-lang`
89    let mut seed = None;
90    loop {
91        let k = rfc6979_generate_k(message_hash, private_key, seed.as_ref());
92
93        match sign(private_key, message_hash, &k) {
94            Ok(sig) => {
95                return Ok(sig);
96            }
97            Err(SignError::InvalidMessageHash) => {
98                return Err(EcdsaSignError::MessageHashOutOfRange)
99            }
100            Err(SignError::InvalidK) => {
101                // Bump seed and retry
102                seed = match seed {
103                    Some(prev_seed) => Some(prev_seed + Felt::ONE),
104                    None => Some(Felt::ONE),
105                };
106            }
107        };
108    }
109}
110
111/// Verified an ECDSA signature on the STARK curve.
112pub fn ecdsa_verify(
113    public_key: &Felt,
114    message_hash: &Felt,
115    signature: &Signature,
116) -> Result<bool, EcdsaVerifyError> {
117    match verify(public_key, message_hash, &signature.r, &signature.s) {
118        Ok(result) => Ok(result),
119        Err(VerifyError::InvalidMessageHash) => Err(EcdsaVerifyError::MessageHashOutOfRange),
120        Err(VerifyError::InvalidPublicKey) => Err(EcdsaVerifyError::InvalidPublicKey),
121        Err(VerifyError::InvalidR) => Err(EcdsaVerifyError::SignatureROutOfRange),
122        Err(VerifyError::InvalidS) => Err(EcdsaVerifyError::SignatureSOutOfRange),
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
132    fn test_compute_hash_on_elements() {
133        // Generated with `cairo-lang`
134        let hash = compute_hash_on_elements(&[
135            Felt::from_hex("0xaa").unwrap(),
136            Felt::from_hex("0xbb").unwrap(),
137            Felt::from_hex("0xcc").unwrap(),
138            Felt::from_hex("0xdd").unwrap(),
139        ]);
140        let expected_hash =
141            Felt::from_hex("025cde77210b1c223b2c6e69db6e9021aa1599177ab177474d5326cd2a62cb69")
142                .unwrap();
143
144        assert_eq!(expected_hash, hash);
145    }
146
147    #[test]
148    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
149    fn test_compute_hash_on_elements_empty_data() {
150        // Generated with `cairo-lang`
151        let hash = compute_hash_on_elements(&[]);
152        let expected_hash =
153            Felt::from_hex("049ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804")
154                .unwrap();
155
156        assert_eq!(expected_hash, hash);
157    }
158
159    #[test]
160    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
161    fn test_ecdsa_sign() {
162        // Generated with `cairo-lang`
163        let signature = ecdsa_sign(
164            &Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
165                .unwrap(),
166            &Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
167                .unwrap(),
168        )
169        .unwrap();
170        let expected_r =
171            Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
172                .unwrap();
173        let expected_s =
174            Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
175                .unwrap();
176
177        assert_eq!(signature.r, expected_r);
178        assert_eq!(signature.s, expected_s);
179    }
180
181    #[test]
182    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
183    fn test_ecdsa_sign_message_hash_out_of_range() {
184        match ecdsa_sign(
185            &Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
186                .unwrap(),
187            &Felt::from_hex("0800000000000000000000000000000000000000000000000000000000000000")
188                .unwrap(),
189        ) {
190            Err(EcdsaSignError::MessageHashOutOfRange) => {}
191            _ => panic!("Should throw error on out of range message hash"),
192        };
193    }
194
195    #[test]
196    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
197    fn test_ecdsa_verify_valid_signature() {
198        // Generated with `cairo-lang`
199        let public_key =
200            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
201                .unwrap();
202        let message_hash =
203            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
204                .unwrap();
205        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
206            .unwrap();
207        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
208            .unwrap();
209
210        assert!(ecdsa_verify(&public_key, &message_hash, &Signature { r, s }).unwrap());
211    }
212
213    #[test]
214    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
215    fn test_ecdsa_verify_invalid_signature() {
216        // Generated with `cairo-lang`
217        let public_key =
218            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
219                .unwrap();
220        let message_hash =
221            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
222                .unwrap();
223        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
224            .unwrap();
225        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9b")
226            .unwrap();
227
228        assert!(!ecdsa_verify(&public_key, &message_hash, &Signature { r, s }).unwrap());
229    }
230}