1use 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 let phrase_with_derivations = format!("{ALICE_WORDS}{derivation}");
153
154 let path_and_pwd = format!("{derivation}///{password}");
156
157 let msg = b"super important thing to sign";
159
160 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 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 let full_derivation = cut_path(&path_and_pwd).unwrap();
176
177 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 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 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 let phrase_with_derivations = format!("{ALICE_WORDS}{derivation}");
225
226 let path_and_pwd = format!("{derivation}///{password}");
228
229 let msg = b"super important thing to sign";
231
232 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 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 let full_derivation = cut_path(&path_and_pwd).unwrap();
247
248 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 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}