Skip to content

Commit 3a40ecb

Browse files
authored
Merge pull request open-wallet-standard#128 from franklad/test/bip32-spec-vectors
fix: add BIP-32/SLIP-10 spec test vectors and seed length validation
2 parents 1de78a3 + eb5b090 commit 3a40ecb

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

  • ows/crates/ows-signer/src

ows/crates/ows-signer/src/hd.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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).
2124
pub struct HdDeriver;
2225

2326
impl 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

Comments
 (0)