substrate_crypto_light/
sr25519.rs

1//! Sr25519, mostly follows `sp_core::sr25519`.
2//! Also supports external Rng.
3
4use parity_scale_codec::{Decode, Encode};
5use rand_core::{CryptoRng, RngCore};
6use schnorrkel::{
7    context::attach_rng,
8    derive::{ChainCode, Derivation},
9    signing_context, ExpansionMode, Keypair, MiniSecretKey,
10};
11use zeroize::{Zeroize, ZeroizeOnDrop};
12
13use crate::{
14    common::{entropy_to_big_seed, DeriveJunction, FullDerivation, HASH_256_LEN},
15    error::Error,
16};
17
18pub const SIGNING_CTX: &[u8] = b"substrate";
19pub const PUBLIC_LEN: usize = 32;
20pub const SIGNATURE_LEN: usize = 64;
21
22#[derive(Clone, Copy, Decode, Debug, Encode, Eq, Ord, PartialEq, PartialOrd)]
23pub struct Public(pub [u8; PUBLIC_LEN]);
24
25impl Public {
26    pub fn verify(&self, msg: &[u8], signature: &Signature) -> bool {
27        let Ok(signature) = schnorrkel::Signature::from_bytes(signature.0.as_ref()) else {
28            return false;
29        };
30        let Ok(public) = schnorrkel::PublicKey::from_bytes(self.0.as_ref()) else {
31            return false;
32        };
33        public.verify_simple(SIGNING_CTX, msg, &signature).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(Zeroize, ZeroizeOnDrop)]
41pub struct Pair(Keypair);
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 mini_secret_bytes = &big_seed[..HASH_256_LEN];
47        let pair = Pair(
48            MiniSecretKey::from_bytes(mini_secret_bytes)
49                .expect("static length, always fits")
50                .expand_to_keypair(ExpansionMode::Ed25519),
51        );
52        big_seed.zeroize();
53        Ok(pair)
54    }
55
56    pub fn from_entropy_and_full_derivation(
57        entropy: &[u8],
58        full_derivation: FullDerivation,
59    ) -> Result<Self, Error> {
60        let mut pair = Self::from_entropy_and_pwd(entropy, full_derivation.password.unwrap_or(""))?;
61        for junction in full_derivation.junctions.iter() {
62            match junction {
63                DeriveJunction::Hard(inner) => {
64                    pair = Pair(
65                        pair.0
66                            .hard_derive_mini_secret_key(Some(ChainCode(*inner)), b"")
67                            .0
68                            .expand_to_keypair(ExpansionMode::Ed25519),
69                    );
70                }
71                DeriveJunction::Soft(inner) => {
72                    pair = Pair(pair.0.derived_key_simple(ChainCode(*inner), []).0);
73                }
74            }
75        }
76        Ok(pair)
77    }
78
79    pub fn from_entropy_and_full_derivation_external_rng<R>(
80        entropy: &[u8],
81        full_derivation: FullDerivation,
82        external_rng: &mut R,
83    ) -> Result<Self, Error>
84    where
85        R: CryptoRng + RngCore,
86    {
87        let mut pair = Self::from_entropy_and_pwd(entropy, full_derivation.password.unwrap_or(""))?;
88        for junction in full_derivation.junctions.iter() {
89            match junction {
90                DeriveJunction::Hard(inner) => {
91                    pair = Pair(
92                        pair.0
93                            .hard_derive_mini_secret_key(Some(ChainCode(*inner)), b"")
94                            .0
95                            .expand_to_keypair(ExpansionMode::Ed25519),
96                    );
97                }
98                DeriveJunction::Soft(inner) => {
99                    pair = Pair(
100                        pair.0
101                            .derived_key_simple_rng(ChainCode(*inner), [], &mut *external_rng)
102                            .0,
103                    );
104                }
105            }
106        }
107        Ok(pair)
108    }
109
110    pub fn sign(&self, msg: &[u8]) -> Signature {
111        let context = signing_context(SIGNING_CTX);
112        Signature(self.0.sign(context.bytes(msg)).to_bytes())
113    }
114
115    pub fn sign_external_rng<R>(&self, msg: &[u8], external_rng: &mut R) -> Signature
116    where
117        R: CryptoRng + RngCore,
118    {
119        let context = signing_context(SIGNING_CTX);
120        Signature(
121            self.0
122                .sign(attach_rng(context.bytes(msg), external_rng))
123                .to_bytes(),
124        )
125    }
126
127    pub fn public(&self) -> Public {
128        Public(self.0.public.to_bytes())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134
135    use mnemonic_external::{regular::InternalWordList, WordSet};
136    use rand_core::{CryptoRng, RngCore};
137    use sp_core::{crypto::Pair, sr25519};
138
139    #[cfg(feature = "std")]
140    use std::format;
141
142    #[cfg(not(feature = "std"))]
143    use alloc::format;
144
145    use crate::common::{cut_path, ALICE_WORDS};
146    use crate::sr25519::{
147        Pair as Sr25529Pair, Public as Sr25519Public, Signature as Sr25519Signature,
148    };
149
150    fn identical_sr25519(derivation: &str, password: &str) {
151        // phrase and full derivation, for `sp-core` procedure
152        let phrase_with_derivations = format!("{ALICE_WORDS}{derivation}");
153
154        // path and password combined, for `substrate-crypto-light` procedure
155        let path_and_pwd = format!("{derivation}///{password}");
156
157        // bytes to sign
158        let msg = b"super important thing to sign";
159
160        // `sr25519` pair, public, and signature with `sp_core`
161        let pair_from_core =
162            sr25519::Pair::from_string(&phrase_with_derivations, Some(password)).unwrap();
163        let public_from_core = pair_from_core.public().0;
164        let signature_from_core = pair_from_core.sign(msg).0;
165
166        // phrase-to-entropy, with `mnemonic-external`
167        let internal_word_list = InternalWordList;
168        let mut word_set = WordSet::new();
169        for word in ALICE_WORDS.split(' ') {
170            word_set.add_word(word, &internal_word_list).unwrap();
171        }
172        let entropy = word_set.to_entropy().unwrap();
173
174        // full derivation, `substrate-crypto-light`
175        let full_derivation = cut_path(&path_and_pwd).unwrap();
176
177        // `sr25519` pair, public, and signature with `substrate-crypto-light`
178        let pair =
179            Sr25529Pair::from_entropy_and_full_derivation(&entropy, full_derivation).unwrap();
180        let public = pair.public().0;
181        let signature = pair.sign(msg).0;
182
183        assert_eq!(public_from_core, public);
184
185        // verify signature made with `substrate-crypto-light` using tools of
186        // `sp-core`
187        let signature_import_into_core = sr25519::Signature::from_raw(signature);
188        let public_import_into_core = sr25519::Public::from_raw(public);
189        assert!(sr25519::Pair::verify(
190            &signature_import_into_core,
191            msg,
192            &public_import_into_core
193        ));
194
195        // verify signature made with tools of `sp-core` using tools of
196        // `substrate-crypto-light`
197        let signature_import = Sr25519Signature(signature_from_core);
198        let public_import = Sr25519Public(public_from_core);
199        assert!(public_import.verify(msg, &signature_import));
200    }
201
202    struct DummyRng;
203
204    impl RngCore for DummyRng {
205        fn next_u32(&mut self) -> u32 {
206            0u32
207        }
208        fn next_u64(&mut self) -> u64 {
209            0u64
210        }
211        fn fill_bytes(&mut self, dest: &mut [u8]) {
212            zeroize::Zeroize::zeroize(dest);
213        }
214        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
215            self.fill_bytes(dest);
216            Ok(())
217        }
218    }
219
220    impl CryptoRng for DummyRng {}
221
222    fn identical_sr25519_external_rng(derivation: &str, password: &str) {
223        // phrase and full derivation, for `sp-core` procedure
224        let phrase_with_derivations = format!("{ALICE_WORDS}{derivation}");
225
226        // path and password combined, for `substrate-crypto-light` procedure
227        let path_and_pwd = format!("{derivation}///{password}");
228
229        // bytes to sign
230        let msg = b"super important thing to sign";
231
232        // `sr25519` pair, public, and signature with `sp_core`
233        let pair_from_core =
234            sr25519::Pair::from_string(&phrase_with_derivations, Some(password)).unwrap();
235        let public_from_core = pair_from_core.public().0;
236
237        // phrase-to-entropy, with `mnemonic-external`
238        let internal_word_list = InternalWordList;
239        let mut word_set = WordSet::new();
240        for word in ALICE_WORDS.split(' ') {
241            word_set.add_word(word, &internal_word_list).unwrap();
242        }
243        let entropy = word_set.to_entropy().unwrap();
244
245        // full derivation, `substrate-crypto-light`
246        let full_derivation = cut_path(&path_and_pwd).unwrap();
247
248        // `sr25519` pair, public, and signature with `substrate-crypto-light`
249        let mut rng = DummyRng;
250        let pair = Sr25529Pair::from_entropy_and_full_derivation_external_rng(
251            &entropy,
252            full_derivation,
253            &mut rng,
254        )
255        .unwrap();
256        let public = pair.public().0;
257        let signature = pair.sign_external_rng(msg, &mut rng).0;
258
259        assert_eq!(public_from_core, public);
260
261        // verify signature made with `substrate-crypto-light` using tools of
262        // `sp-core`
263        let signature_import_into_core = sr25519::Signature::from_raw(signature);
264        let public_import_into_core = sr25519::Public::from_raw(public);
265        assert!(sr25519::Pair::verify(
266            &signature_import_into_core,
267            msg,
268            &public_import_into_core
269        ));
270    }
271
272    #[test]
273    fn test_identical_sr25519_1() {
274        identical_sr25519(
275            "//hard/soft//alicealicealicealicealicealicealicealice",
276            "trickytrick",
277        )
278    }
279
280    #[test]
281    fn test_identical_sr25519_2() {
282        identical_sr25519("//1", "pwd")
283    }
284
285    #[test]
286    fn test_identical_sr25519_1_external() {
287        identical_sr25519_external_rng(
288            "//hard/soft//alicealicealicealicealicealicealicealice",
289            "trickytrick",
290        )
291    }
292
293    #[test]
294    fn test_identical_sr25519_2_external() {
295        identical_sr25519_external_rng("//1", "pwd")
296    }
297}