1mod path;
56
57pub use path::{
58 ChildIndex, ChildIndexError, ChildIndexParseError, DerivationPath, DerivationPathParseError,
59};
60
61use ed25519_dalek::{SigningKey, VerifyingKey};
62use hmac::{Hmac, KeyInit, Mac};
63use sha2::Sha512;
64use std::fmt;
65use zeroize::Zeroizing;
66
67type HmacSha512 = Hmac<Sha512>;
68
69const ED25519_CURVE: &[u8] = b"ed25519 seed";
72
73pub const MIN_SEED_LEN: usize = 16;
81
82#[derive(Debug, thiserror::Error)]
84pub enum Slip10Error {
85 #[error("expected hardened child index: {0}")]
88 ExpectedHardenedIndex(ChildIndex),
89 #[error("master seed is {got} bytes, need at least {min}")]
91 SeedTooShort {
92 got: usize,
94 min: usize,
96 },
97}
98
99pub struct ExtendedSigningKey {
106 pub depth: u8,
108 pub child_index: ChildIndex,
111 pub signing_key: SigningKey,
113 pub chain_code: [u8; 32],
115}
116
117impl ExtendedSigningKey {
118 pub fn from_seed(seed: &[u8]) -> Result<Self, Slip10Error> {
122 if seed.len() < MIN_SEED_LEN {
123 return Err(Slip10Error::SeedTooShort {
124 got: seed.len(),
125 min: MIN_SEED_LEN,
126 });
127 }
128
129 let mut mac = <HmacSha512 as KeyInit>::new_from_slice(ED25519_CURVE)
130 .expect("HMAC accepts a key of any length");
131 mac.update(seed);
132
133 Ok(Self::split(&mac_output(mac), 0, ChildIndex::Normal(0)))
134 }
135
136 pub fn derive<P: AsRef<[ChildIndex]>>(&self, path: &P) -> Result<Self, Slip10Error> {
138 let mut next = self.clone();
139 for index in path.as_ref() {
140 next = next.derive_child(*index)?;
141 }
142 Ok(next)
143 }
144
145 pub fn derive_child(&self, index: ChildIndex) -> Result<Self, Slip10Error> {
149 if index.is_normal() {
150 return Err(Slip10Error::ExpectedHardenedIndex(index));
151 }
152
153 let mut mac = <HmacSha512 as KeyInit>::new_from_slice(&self.chain_code)
154 .expect("HMAC accepts a key of any length");
155 mac.update(&[0u8]);
157 mac.update(self.signing_key.as_bytes());
158 mac.update(&index.to_bits().to_be_bytes());
159
160 Ok(Self::split(
161 &mac_output(mac),
162 self.depth.wrapping_add(1),
163 index,
164 ))
165 }
166
167 #[inline]
169 pub fn verifying_key(&self) -> VerifyingKey {
170 self.signing_key.verifying_key()
171 }
172
173 fn split(bytes: &[u8; 64], depth: u8, child_index: ChildIndex) -> Self {
175 let mut key = Zeroizing::new([0u8; 32]);
176 key.copy_from_slice(&bytes[..32]);
177
178 let mut chain_code = [0u8; 32];
179 chain_code.copy_from_slice(&bytes[32..]);
180
181 Self {
182 depth,
183 child_index,
184 signing_key: SigningKey::from_bytes(&key),
185 chain_code,
186 }
187 }
188}
189
190impl Clone for ExtendedSigningKey {
191 fn clone(&self) -> Self {
192 Self {
193 depth: self.depth,
194 child_index: self.child_index,
195 signing_key: SigningKey::from_bytes(self.signing_key.as_bytes()),
196 chain_code: self.chain_code,
197 }
198 }
199}
200
201impl fmt::Debug for ExtendedSigningKey {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.debug_struct("ExtendedSigningKey")
207 .field("depth", &self.depth)
208 .field("child_index", &self.child_index)
209 .field("signing_key", &"<redacted>")
210 .field("chain_code", &"<redacted>")
211 .finish()
212 }
213}
214
215fn mac_output(mac: HmacSha512) -> Zeroizing<[u8; 64]> {
221 let mut out = Zeroizing::new([0u8; 64]);
222 out.copy_from_slice(&mac.finalize().into_bytes());
223 out
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn hex32(s: &str) -> [u8; 32] {
231 let v = hex::decode(s).expect("valid hex");
232 v.try_into().expect("32 bytes")
233 }
234
235 fn root(seed_hex: &str) -> ExtendedSigningKey {
236 ExtendedSigningKey::from_seed(&hex::decode(seed_hex).expect("valid hex")).expect("root key")
237 }
238
239 fn assert_node(
241 node: &ExtendedSigningKey,
242 depth: u8,
243 child_index: ChildIndex,
244 chain_code: &str,
245 secret: &str,
246 public: &str,
247 ) {
248 assert_eq!(node.depth, depth, "depth");
249 assert_eq!(node.child_index, child_index, "child index");
250 assert_eq!(node.chain_code, hex32(chain_code), "chain code");
251 assert_eq!(node.signing_key.to_bytes(), hex32(secret), "private key");
252 assert_eq!(node.verifying_key().to_bytes(), hex32(public), "public key");
253 }
254
255 #[test]
262 fn slip10_test_vector_1_ed25519() {
263 let node = root("000102030405060708090a0b0c0d0e0f");
264 assert_node(
265 &node,
266 0,
267 ChildIndex::Normal(0),
268 "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb",
269 "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7",
270 "a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed",
271 );
272
273 let node = node.derive_child(ChildIndex::Hardened(0)).unwrap();
274 assert_node(
275 &node,
276 1,
277 ChildIndex::Hardened(0),
278 "8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69",
279 "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",
280 "8c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c",
281 );
282
283 let node = node.derive_child(ChildIndex::Hardened(1)).unwrap();
284 assert_node(
285 &node,
286 2,
287 ChildIndex::Hardened(1),
288 "a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14",
289 "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",
290 "1932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187",
291 );
292
293 let node = node.derive_child(ChildIndex::Hardened(2)).unwrap();
294 assert_node(
295 &node,
296 3,
297 ChildIndex::Hardened(2),
298 "2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c",
299 "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",
300 "ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1",
301 );
302
303 let node = node.derive_child(ChildIndex::Hardened(2)).unwrap();
304 assert_node(
305 &node,
306 4,
307 ChildIndex::Hardened(2),
308 "8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc",
309 "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",
310 "8abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c",
311 );
312
313 let node = node.derive_child(ChildIndex::Hardened(1000000000)).unwrap();
314 assert_node(
315 &node,
316 5,
317 ChildIndex::Hardened(1000000000),
318 "68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230",
319 "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",
320 "3c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a",
321 );
322 }
323
324 #[test]
325 fn slip10_test_vector_2_ed25519() {
326 let node = root(
327 "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c99969390\
328 8d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
329 );
330 assert_node(
331 &node,
332 0,
333 ChildIndex::Normal(0),
334 "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b",
335 "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012",
336 "8fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a",
337 );
338
339 let node = node.derive_child(ChildIndex::Hardened(0)).unwrap();
340 assert_node(
341 &node,
342 1,
343 ChildIndex::Hardened(0),
344 "0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d",
345 "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635",
346 "86fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037",
347 );
348
349 let node = node.derive_child(ChildIndex::Hardened(2147483647)).unwrap();
350 assert_node(
351 &node,
352 2,
353 ChildIndex::Hardened(2147483647),
354 "138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f",
355 "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4",
356 "5ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d",
357 );
358
359 let node = node.derive_child(ChildIndex::Hardened(1)).unwrap();
360 assert_node(
361 &node,
362 3,
363 ChildIndex::Hardened(1),
364 "73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90",
365 "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c",
366 "2e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45",
367 );
368
369 let node = node.derive_child(ChildIndex::Hardened(2147483646)).unwrap();
370 assert_node(
371 &node,
372 4,
373 ChildIndex::Hardened(2147483646),
374 "0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a",
375 "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72",
376 "e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b",
377 );
378
379 let node = node.derive_child(ChildIndex::Hardened(2)).unwrap();
380 assert_node(
381 &node,
382 5,
383 ChildIndex::Hardened(2),
384 "5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4",
385 "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d",
386 "47150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0",
387 );
388 }
389
390 #[test]
392 fn derive_by_path_matches_stepwise_derivation() {
393 let node = root("000102030405060708090a0b0c0d0e0f");
394
395 let path: DerivationPath = "m/0'/1'/2'/2'/1000000000'".parse().unwrap();
396 let by_path = node.derive(&path).unwrap();
397
398 let mut stepwise = node.clone();
399 for index in [0, 1, 2, 2, 1000000000] {
400 stepwise = stepwise.derive_child(ChildIndex::Hardened(index)).unwrap();
401 }
402
403 assert_eq!(
404 by_path.signing_key.to_bytes(),
405 stepwise.signing_key.to_bytes()
406 );
407 assert_eq!(by_path.chain_code, stepwise.chain_code);
408 assert_eq!(by_path.depth, 5);
409 }
410
411 #[test]
412 fn the_empty_path_is_the_master_key() {
413 let node = root("000102030405060708090a0b0c0d0e0f");
414 let path: DerivationPath = "m".parse().unwrap();
415 let derived = node.derive(&path).unwrap();
416
417 assert_eq!(derived.signing_key.to_bytes(), node.signing_key.to_bytes());
418 assert_eq!(derived.chain_code, node.chain_code);
419 assert_eq!(derived.depth, 0);
420 }
421
422 #[test]
428 fn a_normal_child_index_is_refused() {
429 let node = root("000102030405060708090a0b0c0d0e0f");
430
431 assert!(matches!(
432 node.derive_child(ChildIndex::Normal(0)),
433 Err(Slip10Error::ExpectedHardenedIndex(ChildIndex::Normal(0)))
434 ));
435 assert!(matches!(
436 node.derive_child(ChildIndex::Normal(100000)),
437 Err(Slip10Error::ExpectedHardenedIndex(ChildIndex::Normal(
438 100000
439 )))
440 ));
441 }
442
443 #[test]
444 fn a_normal_index_mid_path_is_refused() {
445 let node = root("000102030405060708090a0b0c0d0e0f");
446 let soft_path: DerivationPath = "m/0'/1'/2'/3/4'".parse().unwrap();
447
448 assert!(matches!(
449 node.derive(&soft_path),
450 Err(Slip10Error::ExpectedHardenedIndex(ChildIndex::Normal(3)))
451 ));
452 }
453
454 #[test]
459 fn a_short_seed_is_refused() {
460 let err = ExtendedSigningKey::from_seed(&[0u8; 15]).unwrap_err();
461 assert!(matches!(
462 err,
463 Slip10Error::SeedTooShort { got: 15, min: 16 }
464 ));
465 assert!(ExtendedSigningKey::from_seed(&[0u8; 16]).is_ok());
467 }
468
469 #[test]
470 fn the_seed_lengths_this_workspace_actually_uses_are_accepted() {
471 assert!(ExtendedSigningKey::from_seed(&[7u8; 32]).is_ok());
473 assert!(ExtendedSigningKey::from_seed(&[7u8; 64]).is_ok());
474 }
475
476 #[test]
477 fn different_seeds_give_different_master_keys() {
478 let a = ExtendedSigningKey::from_seed(&[1u8; 32]).unwrap();
479 let b = ExtendedSigningKey::from_seed(&[2u8; 32]).unwrap();
480 assert_ne!(a.signing_key.to_bytes(), b.signing_key.to_bytes());
481 assert_ne!(a.chain_code, b.chain_code);
482 }
483
484 #[test]
485 fn derivation_is_deterministic_across_calls() {
486 let path: DerivationPath = "m/26'/2'/0'/1'".parse().unwrap();
487 let first = ExtendedSigningKey::from_seed(&[9u8; 32])
488 .unwrap()
489 .derive(&path)
490 .unwrap();
491 let second = ExtendedSigningKey::from_seed(&[9u8; 32])
492 .unwrap()
493 .derive(&path)
494 .unwrap();
495
496 assert_eq!(first.signing_key.to_bytes(), second.signing_key.to_bytes());
497 assert_eq!(first.chain_code, second.chain_code);
498 }
499
500 #[test]
501 fn sibling_paths_give_independent_keys() {
502 let root = ExtendedSigningKey::from_seed(&[3u8; 32]).unwrap();
503 let a = root
504 .derive(&"m/26'/2'/0'/0'".parse::<DerivationPath>().unwrap())
505 .unwrap();
506 let b = root
507 .derive(&"m/26'/2'/0'/1'".parse::<DerivationPath>().unwrap())
508 .unwrap();
509
510 assert_ne!(a.signing_key.to_bytes(), b.signing_key.to_bytes());
511 assert_ne!(a.chain_code, b.chain_code);
512 }
513
514 #[test]
519 fn clone_preserves_the_whole_node() {
520 let node = root("000102030405060708090a0b0c0d0e0f")
521 .derive_child(ChildIndex::Hardened(26))
522 .unwrap();
523 let cloned = node.clone();
524
525 assert_eq!(cloned.depth, node.depth);
526 assert_eq!(cloned.child_index, node.child_index);
527 assert_eq!(cloned.chain_code, node.chain_code);
528 assert_eq!(cloned.signing_key.to_bytes(), node.signing_key.to_bytes());
529 }
530
531 #[test]
533 fn debug_does_not_leak_key_material() {
534 let node = root("000102030405060708090a0b0c0d0e0f");
535 let rendered = format!("{node:?}");
536
537 assert!(rendered.contains("<redacted>"));
538 assert!(
539 !rendered.contains("2b4be7f1"),
540 "private key leaked into Debug output: {rendered}"
541 );
542 assert!(
544 !rendered.contains("90046a93"),
545 "chain code leaked into Debug output: {rendered}"
546 );
547 }
548}