substrate_crypto_light/
ecdsa.rs

1#[cfg(feature = "std")]
2use std::vec;
3
4#[cfg(not(feature = "std"))]
5use alloc::vec;
6
7use k256::ecdsa::{signature::hazmat::PrehashVerifier, SigningKey, VerifyingKey};
8use parity_scale_codec::{Decode, Encode};
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use crate::{
12    common::{blake2_256, entropy_to_big_seed, DeriveJunction, FullDerivation, HASH_256_LEN},
13    error::Error,
14};
15
16pub const ID: &str = "Secp256k1HDKD";
17pub const PUBLIC_LEN: usize = 33;
18pub const SIGNATURE_LEN: usize = 65;
19
20#[derive(Clone, Copy, Decode, Debug, Encode, Eq, Ord, PartialEq, PartialOrd)]
21pub struct Public(pub [u8; PUBLIC_LEN]);
22
23impl Public {
24    pub fn verify(&self, msg: &[u8], signature: &Signature) -> bool {
25        let Ok(signature) = k256::ecdsa::Signature::from_slice(signature.0[..64].as_ref()) else {
26            return false;
27        };
28        let Ok(verifying_key) = VerifyingKey::from_sec1_bytes(self.0.as_ref()) else {
29            return false;
30        };
31        verifying_key
32            .verify_prehash(&blake2_256(msg), &signature)
33            .is_ok()
34    }
35}
36
37#[derive(Clone, Copy, Decode, Debug, Encode, Eq, Ord, PartialEq, PartialOrd)]
38pub struct Signature(pub [u8; SIGNATURE_LEN]);
39
40#[derive(ZeroizeOnDrop)]
41pub struct Pair(SigningKey);
42
43impl Pair {
44    pub fn from_entropy_and_pwd(entropy: &[u8], pwd: &str) -> Result<Self, Error> {
45        let mut big_seed = entropy_to_big_seed(entropy, pwd)?;
46        let seed = &big_seed[..HASH_256_LEN];
47        let signing_key_result = SigningKey::from_bytes(seed.as_ref().into());
48        big_seed.zeroize();
49        match signing_key_result {
50            Ok(signing_key) => Ok(Pair(signing_key)),
51            Err(_) => Err(Error::EcdsaPairGen),
52        }
53    }
54
55    pub fn from_entropy_and_full_derivation(
56        entropy: &[u8],
57        full_derivation: FullDerivation,
58    ) -> Result<Self, Error> {
59        let mut pair = Self::from_entropy_and_pwd(entropy, full_derivation.password.unwrap_or(""))?;
60        for junction in full_derivation.junctions.iter() {
61            match junction {
62                DeriveJunction::Hard(inner) => {
63                    // derivation mixing is done with hash updates, as opposed
64                    // to `using_encoded`, to avoid multiple secret copying
65                    let mut blake2b_state = blake2b_simd::Params::new()
66                        .hash_length(HASH_256_LEN)
67                        .to_state();
68                    blake2b_state.update(&ID.encode());
69                    blake2b_state.update(pair.0.to_bytes().as_slice());
70                    blake2b_state.update(inner);
71                    let bytes = blake2b_state.finalize();
72                    pair = Pair(
73                        SigningKey::from_bytes(bytes.as_ref().into())
74                            .map_err(|_| Error::EcdsaPairGen)?,
75                    );
76                }
77                DeriveJunction::Soft(_) => return Err(Error::NoSoftDerivationEcdsa),
78            }
79        }
80        Ok(pair)
81    }
82
83    pub fn sign(&self, msg: &[u8]) -> Result<Signature, Error> {
84        let (signature_ecdsa, recid) = self
85            .0
86            .sign_prehash_recoverable(&blake2_256(msg))
87            .map_err(|_| Error::EcdsaSignatureGen)?;
88        Ok(Signature(
89            [
90                signature_ecdsa.to_bytes().as_slice().to_vec(),
91                vec![recid.to_byte()],
92            ]
93            .concat()
94            .try_into()
95            .map_err(|_| Error::EcdsaSignatureLength)?,
96        ))
97    }
98
99    pub fn public(&self) -> Result<Public, Error> {
100        Ok(Public(
101            self.0
102                .verifying_key()
103                .to_encoded_point(true)
104                .as_bytes()
105                .try_into()
106                .map_err(|_| Error::EcdsaPublicKeyLength)?,
107        ))
108    }
109}
110
111#[cfg(feature = "std")]
112#[cfg(test)]
113mod tests {
114
115    use mnemonic_external::{regular::InternalWordList, WordSet};
116    use sp_core::{crypto::Pair, ecdsa};
117
118    #[cfg(feature = "std")]
119    use std::format;
120
121    #[cfg(not(feature = "std"))]
122    use alloc::format;
123
124    use crate::common::{cut_path, ALICE_WORDS};
125    use crate::ecdsa::{Pair as EcdsaPair, Public as EcdsaPublic, Signature as EcdsaSignature};
126
127    fn identical_ecdsa(derivation: &str, password: &str) {
128        // phrase and full derivation, for `sp-core` procedure
129        let phrase_with_derivations = format!("{ALICE_WORDS}{derivation}");
130
131        // path and password combined, for `substrate-crypto-light` procedure
132        let path_and_pwd = format!("{derivation}///{password}");
133
134        // bytes to sign
135        let msg = b"super important thing to sign";
136
137        // `ecdsa` pair, public, and signature with `sp_core`
138        let pair_from_core =
139            ecdsa::Pair::from_string(&phrase_with_derivations, Some(password)).unwrap();
140        let public_from_core = pair_from_core.public().0;
141        let signature_from_core = pair_from_core.sign(msg).0;
142
143        // phrase-to-entropy, with `mnemonic-external`
144        let internal_word_list = InternalWordList;
145        let mut word_set = WordSet::new();
146        for word in ALICE_WORDS.split(' ') {
147            word_set.add_word(word, &internal_word_list).unwrap();
148        }
149        let entropy = word_set.to_entropy().unwrap();
150
151        // full derivation, `substrate-crypto-light`
152        let full_derivation = cut_path(&path_and_pwd).unwrap();
153
154        // `ecdsa` pair, public, and signature with `substrate-crypto-light`
155        let pair = EcdsaPair::from_entropy_and_full_derivation(&entropy, full_derivation).unwrap();
156        let public = pair.public().unwrap().0;
157        let signature = pair.sign(msg).unwrap().0;
158
159        assert_eq!(public_from_core, public);
160
161        // verify signature made with `substrate-crypto-light` using tools of
162        // `sp-core`
163        let signature_import_into_core = ecdsa::Signature::from_raw(signature);
164        let public_import_into_core = ecdsa::Public::from_raw(public);
165        assert!(ecdsa::Pair::verify(
166            &signature_import_into_core,
167            msg,
168            &public_import_into_core
169        ));
170
171        // verify signature made with tools of `sp-core` using tools of
172        // `substrate-crypto-light`
173        let signature_import = EcdsaSignature(signature_from_core);
174        let public_import = EcdsaPublic(public_from_core);
175        assert!(public_import.verify(msg, &signature_import));
176    }
177
178    #[test]
179    fn test_identical_ecdsa_1() {
180        identical_ecdsa(
181            "//hard//alicealicealicealicealicealicealicealice",
182            "trickytrick",
183        )
184    }
185
186    #[test]
187    fn test_identical_ecdsa_2() {
188        identical_ecdsa("//1", "pwd")
189    }
190}