starknet_rust_signers/
key_pair.rs

1use crypto_bigint::{Encoding, NonZero, U256};
2use rand::{rngs::StdRng, Rng, SeedableRng};
3use starknet_rust_core::{
4    crypto::{ecdsa_sign, ecdsa_verify, EcdsaSignError, EcdsaVerifyError, Signature},
5    types::Felt,
6};
7use starknet_rust_crypto::get_public_key;
8
9/// A ECDSA signing (private) key on the STARK curve.
10#[derive(Debug, Clone)]
11pub struct SigningKey {
12    secret_scalar: Felt,
13}
14
15/// A ECDSA verifying (public) key on the STARK curve.
16#[derive(Debug, Clone)]
17pub struct VerifyingKey {
18    scalar: Felt,
19}
20
21/// Errors using an encrypted JSON keystore.
22#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
23#[derive(Debug, thiserror::Error)]
24pub enum KeystoreError {
25    /// The file path is invalid.
26    #[error("invalid path")]
27    InvalidPath,
28    /// The decrypted secret scalar is not a valid private key.
29    #[error("invalid decrypted secret scalar")]
30    InvalidScalar,
31    /// Upstream `eth-keystore` error propagated.
32    #[error(transparent)]
33    Inner(eth_keystore::KeystoreError),
34}
35
36impl SigningKey {
37    /// Generates a new key pair from a cryptographically secure RNG.
38    pub fn from_random() -> Self {
39        const PRIME: NonZero<U256> = NonZero::from_uint(U256::from_be_hex(
40            "0800000000000011000000000000000000000000000000000000000000000001",
41        ));
42
43        let mut rng = StdRng::from_entropy();
44        let mut buffer = [0u8; 32];
45        rng.fill(&mut buffer);
46
47        let random_u256 = U256::from_be_slice(&buffer);
48        let secret_scalar = random_u256.rem(&PRIME);
49
50        // It's safe to unwrap here as we're 100% sure it's not out of range
51        let secret_scalar = Felt::from_bytes_be_slice(&secret_scalar.to_be_bytes());
52
53        Self { secret_scalar }
54    }
55
56    /// Constructs [`SigningKey`] directly from a secret scalar.
57    pub const fn from_secret_scalar(secret_scalar: Felt) -> Self {
58        Self { secret_scalar }
59    }
60
61    /// Loads the private key from a Web3 Secret Storage Definition keystore.
62    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
63    pub fn from_keystore<P>(path: P, password: &str) -> Result<Self, KeystoreError>
64    where
65        P: AsRef<std::path::Path>,
66    {
67        let key = eth_keystore::decrypt_key(path, password).map_err(KeystoreError::Inner)?;
68        let secret_scalar = Felt::from_bytes_be_slice(&key);
69        Ok(Self::from_secret_scalar(secret_scalar))
70    }
71
72    /// Encrypts and saves the private key to a Web3 Secret Storage Definition JSON file.
73    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
74    pub fn save_as_keystore<P>(&self, path: P, password: &str) -> Result<(), KeystoreError>
75    where
76        P: AsRef<std::path::Path>,
77    {
78        // Work around the issue of `eth-keystore` not supporting full path.
79        // TODO: patch or fork `eth-keystore`
80        let mut path = path.as_ref().to_path_buf();
81        let file_name = path
82            .file_name()
83            .ok_or(KeystoreError::InvalidPath)?
84            .to_str()
85            .ok_or(KeystoreError::InvalidPath)?
86            .to_owned();
87        path.pop();
88
89        let mut rng = StdRng::from_entropy();
90        eth_keystore::encrypt_key(
91            path,
92            &mut rng,
93            self.secret_scalar.to_bytes_be(),
94            password,
95            Some(&file_name),
96        )
97        .map_err(KeystoreError::Inner)?;
98
99        Ok(())
100    }
101
102    /// Gets the secret scalar in the signing key.
103    pub const fn secret_scalar(&self) -> Felt {
104        self.secret_scalar
105    }
106
107    /// Derives the verifying (public) key that corresponds to the signing key.
108    pub fn verifying_key(&self) -> VerifyingKey {
109        VerifyingKey::from_scalar(get_public_key(&self.secret_scalar))
110    }
111
112    /// Signs a raw hash using ECDSA for a signature.
113    pub fn sign(&self, hash: &Felt) -> Result<Signature, EcdsaSignError> {
114        ecdsa_sign(&self.secret_scalar, hash).map(|sig| sig.into())
115    }
116}
117
118impl VerifyingKey {
119    /// Constructs [`VerifyingKey`] directly from a scalar.
120    pub const fn from_scalar(scalar: Felt) -> Self {
121        Self { scalar }
122    }
123
124    /// Gets the scalar in the verifying key.
125    pub const fn scalar(&self) -> Felt {
126        self.scalar
127    }
128
129    /// Verifies that an ECDSA signature is valid for the verifying key against a certain message
130    /// hash.
131    pub fn verify(&self, hash: &Felt, signature: &Signature) -> Result<bool, EcdsaVerifyError> {
132        ecdsa_verify(&self.scalar, hash, signature)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
142    fn test_get_secret_scalar() {
143        // Generated with `cairo-lang`
144        let private_key =
145            Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
146                .unwrap();
147
148        let signing_key = SigningKey::from_secret_scalar(private_key);
149
150        assert_eq!(signing_key.secret_scalar(), private_key);
151    }
152
153    #[test]
154    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
155    fn test_get_verifying_key() {
156        // Generated with `cairo-lang`
157        let private_key =
158            Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
159                .unwrap();
160        let expected_public_key =
161            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
162                .unwrap();
163
164        let signing_key = SigningKey::from_secret_scalar(private_key);
165        let verifying_key = signing_key.verifying_key();
166
167        assert_eq!(verifying_key.scalar(), expected_public_key);
168    }
169
170    #[test]
171    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
172    fn test_sign() {
173        // Generated with `cairo-lang`
174        let private_key =
175            Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
176                .unwrap();
177        let hash =
178            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
179                .unwrap();
180        let expected_r =
181            Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
182                .unwrap();
183        let expected_s =
184            Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
185                .unwrap();
186
187        let signing_key = SigningKey::from_secret_scalar(private_key);
188        let signature = signing_key.sign(&hash).unwrap();
189
190        assert_eq!(signature.r, expected_r);
191        assert_eq!(signature.s, expected_s);
192    }
193
194    #[test]
195    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
196    fn test_hash_out_of_range() {
197        let private_key =
198            Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
199                .unwrap();
200        let hash =
201            Felt::from_hex("0800000000000000000000000000000000000000000000000000000000000000")
202                .unwrap();
203
204        let signing_key = SigningKey::from_secret_scalar(private_key);
205
206        match signing_key.sign(&hash) {
207            Err(EcdsaSignError::MessageHashOutOfRange) => {}
208            _ => panic!("Should throw error on out of range hash"),
209        };
210    }
211
212    #[test]
213    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
214    fn test_verify_valid_signature() {
215        // Generated with `cairo-lang`
216        let public_key =
217            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
218                .unwrap();
219        let hash =
220            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
221                .unwrap();
222        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
223            .unwrap();
224        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
225            .unwrap();
226
227        let verifying_key = VerifyingKey::from_scalar(public_key);
228
229        assert!(verifying_key.verify(&hash, &Signature { r, s }).unwrap());
230    }
231
232    #[test]
233    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
234    fn test_verify_invalid_signature() {
235        // Generated with `cairo-lang`
236        let public_key =
237            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
238                .unwrap();
239        let hash =
240            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
241                .unwrap();
242        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
243            .unwrap();
244        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9b")
245            .unwrap();
246
247        let verifying_key = VerifyingKey::from_scalar(public_key);
248
249        assert!(!verifying_key.verify(&hash, &Signature { r, s }).unwrap());
250    }
251}