@@ -15,14 +15,22 @@ pub enum HdError {
1515
1616 #[ error( "ed25519 requires hardened-only derivation" ) ]
1717 Ed25519NonHardened ,
18+
19+ #[ error( "invalid seed length: expected 16-64 bytes, got {0}" ) ]
20+ InvalidSeedLength ( usize ) ,
1821}
1922
2023/// HD key deriver supporting BIP-32 (secp256k1) and SLIP-10 (ed25519).
2124pub struct HdDeriver ;
2225
2326impl HdDeriver {
2427 /// Derive a child private key from a seed and derivation path.
28+ ///
29+ /// Seed must be 16-64 bytes (BIP-32 §2).
2530 pub fn derive ( seed : & [ u8 ] , path : & str , curve : Curve ) -> Result < SecretBytes , HdError > {
31+ if seed. len ( ) < 16 || seed. len ( ) > 64 {
32+ return Err ( HdError :: InvalidSeedLength ( seed. len ( ) ) ) ;
33+ }
2634 Self :: validate_path ( path) ?;
2735
2836 match curve {
@@ -268,6 +276,204 @@ mod tests {
268276 }
269277 }
270278
279+ // === BIP-32 spec test vectors (secp256k1) ===
280+ // Source: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
281+
282+ #[ test]
283+ fn test_bip32_vector1_chain ( ) {
284+ let seed = hex:: decode ( "000102030405060708090a0b0c0d0e0f" ) . unwrap ( ) ;
285+
286+ let cases = [
287+ (
288+ "m/0'" ,
289+ "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea" ,
290+ ) ,
291+ (
292+ "m/0'/1" ,
293+ "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368" ,
294+ ) ,
295+ (
296+ "m/0'/1/2'" ,
297+ "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca" ,
298+ ) ,
299+ (
300+ "m/0'/1/2'/2" ,
301+ "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4" ,
302+ ) ,
303+ (
304+ "m/0'/1/2'/2/1000000000" ,
305+ "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8" ,
306+ ) ,
307+ ] ;
308+
309+ for ( path, expected_hex) in cases {
310+ let key = HdDeriver :: derive ( & seed, path, Curve :: Secp256k1 )
311+ . unwrap_or_else ( |e| panic ! ( "failed to derive {}: {}" , path, e) ) ;
312+ assert_eq ! (
313+ hex:: encode( key. expose( ) ) ,
314+ expected_hex,
315+ "BIP-32 vector 1 mismatch at {}" ,
316+ path
317+ ) ;
318+ }
319+ }
320+
321+ #[ test]
322+ fn test_bip32_vector2_chain ( ) {
323+ let seed = hex:: decode (
324+ "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2\
325+ 9f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
326+ )
327+ . unwrap ( ) ;
328+
329+ let cases = [
330+ (
331+ "m/0" ,
332+ "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e" ,
333+ ) ,
334+ (
335+ "m/0/2147483647'" ,
336+ "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93" ,
337+ ) ,
338+ (
339+ "m/0/2147483647'/1" ,
340+ "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7" ,
341+ ) ,
342+ (
343+ "m/0/2147483647'/1/2147483646'" ,
344+ "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d" ,
345+ ) ,
346+ (
347+ "m/0/2147483647'/1/2147483646'/2" ,
348+ "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23" ,
349+ ) ,
350+ ] ;
351+
352+ for ( path, expected_hex) in cases {
353+ let key = HdDeriver :: derive ( & seed, path, Curve :: Secp256k1 )
354+ . unwrap_or_else ( |e| panic ! ( "failed to derive {}: {}" , path, e) ) ;
355+ assert_eq ! (
356+ hex:: encode( key. expose( ) ) ,
357+ expected_hex,
358+ "BIP-32 vector 2 mismatch at {}" ,
359+ path
360+ ) ;
361+ }
362+ }
363+
364+ // === SLIP-10 spec test vectors (ed25519) ===
365+ // Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors
366+
367+ #[ test]
368+ fn test_slip10_vector1_chain ( ) {
369+ let seed = hex:: decode ( "000102030405060708090a0b0c0d0e0f" ) . unwrap ( ) ;
370+
371+ let cases = [
372+ (
373+ "m/0'" ,
374+ "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3" ,
375+ ) ,
376+ (
377+ "m/0'/1'" ,
378+ "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2" ,
379+ ) ,
380+ (
381+ "m/0'/1'/2'" ,
382+ "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9" ,
383+ ) ,
384+ (
385+ "m/0'/1'/2'/2'" ,
386+ "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662" ,
387+ ) ,
388+ (
389+ "m/0'/1'/2'/2'/1000000000'" ,
390+ "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793" ,
391+ ) ,
392+ ] ;
393+
394+ for ( path, expected_hex) in cases {
395+ let key = HdDeriver :: derive ( & seed, path, Curve :: Ed25519 )
396+ . unwrap_or_else ( |e| panic ! ( "failed to derive {}: {}" , path, e) ) ;
397+ assert_eq ! (
398+ hex:: encode( key. expose( ) ) ,
399+ expected_hex,
400+ "SLIP-10 vector 1 mismatch at {}" ,
401+ path
402+ ) ;
403+ }
404+ }
405+
406+ #[ test]
407+ fn test_slip10_vector2_chain ( ) {
408+ let seed = hex:: decode (
409+ "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2\
410+ 9f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
411+ )
412+ . unwrap ( ) ;
413+
414+ let cases = [
415+ (
416+ "m/0'" ,
417+ "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635" ,
418+ ) ,
419+ (
420+ "m/0'/2147483647'" ,
421+ "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4" ,
422+ ) ,
423+ (
424+ "m/0'/2147483647'/1'" ,
425+ "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c" ,
426+ ) ,
427+ (
428+ "m/0'/2147483647'/1'/2147483646'" ,
429+ "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72" ,
430+ ) ,
431+ (
432+ "m/0'/2147483647'/1'/2147483646'/2'" ,
433+ "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d" ,
434+ ) ,
435+ ] ;
436+
437+ for ( path, expected_hex) in cases {
438+ let key = HdDeriver :: derive ( & seed, path, Curve :: Ed25519 )
439+ . unwrap_or_else ( |e| panic ! ( "failed to derive {}: {}" , path, e) ) ;
440+ assert_eq ! (
441+ hex:: encode( key. expose( ) ) ,
442+ expected_hex,
443+ "SLIP-10 vector 2 mismatch at {}" ,
444+ path
445+ ) ;
446+ }
447+ }
448+
449+ // === Seed length validation ===
450+
451+ #[ test]
452+ fn test_seed_length_too_short ( ) {
453+ let seed = [ 0u8 ; 15 ] ;
454+ let result = HdDeriver :: derive ( & seed, "m/0'" , Curve :: Secp256k1 ) ;
455+ assert ! ( matches!( result, Err ( HdError :: InvalidSeedLength ( 15 ) ) ) ) ;
456+ }
457+
458+ #[ test]
459+ fn test_seed_length_too_long ( ) {
460+ let seed = [ 0u8 ; 65 ] ;
461+ let result = HdDeriver :: derive ( & seed, "m/0'" , Curve :: Secp256k1 ) ;
462+ assert ! ( matches!( result, Err ( HdError :: InvalidSeedLength ( 65 ) ) ) ) ;
463+ }
464+
465+ #[ test]
466+ fn test_seed_length_minimum_accepted ( ) {
467+ let seed = [ 0u8 ; 16 ] ;
468+ assert ! ( HdDeriver :: derive( & seed, "m/0'" , Curve :: Secp256k1 ) . is_ok( ) ) ;
469+ }
470+
471+ #[ test]
472+ fn test_seed_length_maximum_accepted ( ) {
473+ let seed = [ 0u8 ; 64 ] ;
474+ assert ! ( HdDeriver :: derive( & seed, "m/0'" , Curve :: Secp256k1 ) . is_ok( ) ) ;
475+ }
476+
271477 // === Characterization tests: lock down current behavior before refactoring ===
272478
273479 #[ test]
0 commit comments