diff --git a/Cargo.lock b/Cargo.lock index 702bf52..c290deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,7 @@ version = "0.5.0" dependencies = [ "generic-array", "generic-ec", + "hex", "hex-literal", "hmac", "serde", diff --git a/Cargo.toml b/Cargo.toml index e160814..ee528d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ generic-array = "0.14" serde = { version = "1", default-features = false, features = ["derive"], optional = true } [dev-dependencies] +hex = "0.4" hex-literal = "0.4" [features] diff --git a/src/lib.rs b/src/lib.rs index adeaa5d..1ee2d22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ use generic_array::{ GenericArray, }; use generic_ec::{Curve, Point, Scalar, SecretScalar}; -use hmac::Mac as _; +use hmac::Mac; #[cfg(any( feature = "curve-secp256k1", @@ -685,3 +685,66 @@ fn split_into_two_halves( ) -> (&GenericArray, &GenericArray) { generic_array::sequence::Split::split(i) } + +/// HD derivation for Ed25519 curve +/// +/// This type of derivation isn't defined in any known to us standards, but it can be often +/// found in other libraries. It is secure and efficient (much more efficient than using +/// [`Slip10Like`](Slip10Like), for instance). +pub struct Edwards; + +#[cfg(feature = "curve-ed25519")] +impl DeriveShift for Edwards { + fn derive_public_shift( + parent_public_key: &ExtendedPublicKey, + child_index: NonHardenedIndex, + ) -> DerivedShift { + let hmac = HmacSha512::new_from_slice(&parent_public_key.chain_code) + .expect("this never fails: hmac can handle keys of any size"); + let i = hmac + .clone() + .chain_update(&parent_public_key.public_key.to_bytes(true)) + // we prepend 0 byte to the public key for compatibility with other libs + .chain_update([0x00]) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(parent_public_key, i) + } + + fn derive_hardened_shift( + parent_key: &ExtendedKeyPair, + child_index: HardenedIndex, + ) -> DerivedShift { + let hmac = HmacSha512::new_from_slice(parent_key.chain_code()) + .expect("this never fails: hmac can handle keys of any size"); + let i = hmac + .clone() + .chain_update([0x00]) + .chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes()) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(&parent_key.public_key, i) + } +} + +impl Edwards { + fn calculate_shift( + parent_public_key: &ExtendedPublicKey, + i: hmac::digest::Output, + ) -> DerivedShift { + let (i_left, i_right) = split_into_two_halves(&i); + + let shift = Scalar::from_be_bytes_mod_order(i_left); + let child_pk = parent_public_key.public_key + Point::generator() * shift; + + DerivedShift { + shift, + child_public_key: ExtendedPublicKey { + public_key: child_pk, + chain_code: (*i_right).into(), + }, + } + } +} diff --git a/tests/edwards_test_vector.rs b/tests/edwards_test_vector.rs new file mode 100644 index 0000000..5a9af77 --- /dev/null +++ b/tests/edwards_test_vector.rs @@ -0,0 +1,156 @@ +use hd_wallet::HdWallet; +use hex_literal::hex; + +struct TestVector { + root_secret_key: [u8; 32], + root_public_key: [u8; 32], + chain_code: hd_wallet::ChainCode, + derivations: &'static [Derivation], +} + +struct Derivation { + path: &'static [u32], + + expected_secret_key: [u8; 32], + expected_public_key: [u8; 32], +} + +const TEST_VECTORS: &[TestVector] = &[TestVector { + root_secret_key: hex!("09ba1ad29fabe87a0cf23fec142db2adfb8f9e7089928000dcba5714e08236ec"), + root_public_key: hex!("6fa093b0e855f5fdb40d77f6efe9b67b709092a71d73f35de6afc70cac40d57a"), + chain_code: hex!("64ae4b48b206ef11f75059af10d209546586baf8418222c6f4b989b75d008ddd"), + derivations: &[ + // Non-hardened derivation + Derivation { + path: &[0], + expected_secret_key: hex!( + "0542fcce4962a2e1785bc9d183e6d80f754d7ad70251b7d9239b434bc6b117d4" + ), + expected_public_key: hex!( + "4fefbecb1d4a584b289e457ffe868d87b27ab421f66e32a078616552a837c7e2" + ), + }, + Derivation { + path: &[1], + expected_secret_key: hex!( + "0a25f8ffae098918dff9b24afbe8dae365015f14b4b559f5097684747a62ec97" + ), + expected_public_key: hex!( + "0dc358776f744c4d6c282dbca128d123a0ac38fd7cda5cf87805b82c70a0e2cd" + ), + }, + Derivation { + path: &[2], + expected_secret_key: hex!( + "0b68998b58f5b96ec64b754d421339869a439c6249b843e78610c675d51f01f9" + ), + expected_public_key: hex!( + "d634d478dfb41b9bc8cc6653febd00f89bf5bf8c6f665dbdf541a203abef0882" + ), + }, + Derivation { + path: &[1245290303, 456055179, 1419108629, 261968456], + expected_secret_key: hex!( + "089f32db21f3027a39ee9a6bebae1ffa0bd07527120f5fe943a7d6363bd90ff6" + ), + expected_public_key: hex!( + "4671c7c639c8421d16488a59618bc4d06dbae56741df740eea6be993eb99f734" + ), + }, + Derivation { + path: &[1478344788, 731157828, 912233245, 1553129543], + expected_secret_key: hex!( + "04fe0f016a5b070f49f5b8f76de8862f5520661461b7914463d9ecd81a893f90" + ), + expected_public_key: hex!( + "d983da0a4f2a368bbc5ada8af0c5a003adea602c2e7ad1feca60c73401dc606e" + ), + }, + // Hardened derivation + Derivation { + path: &[0 + hd_wallet::H], + expected_secret_key: hex!( + "098b5d8be3cd71cecf390facd083ca0e3e03cc78a10920094e2cee300f8de291" + ), + expected_public_key: hex!( + "5963e6410d44538fec067ff59d54814de6dfd5daf03d693c655f44e2fd89ae86" + ), + }, + Derivation { + path: &[1 + hd_wallet::H], + expected_secret_key: hex!( + "06928b571aa8659d2976ab000e27f962b62b9d4e61ce6ab76380bd5f9ab6b1f9" + ), + expected_public_key: hex!( + "97d28095b4cc43ef45eb10da1b5c01ff85a0695472252f218c93f21a3ebe8a42" + ), + }, + Derivation { + path: &[2 + hd_wallet::H], + expected_secret_key: hex!( + "009193ef5345093a2c787c93ff3099731a605ffde2836cbc5d4979ed9a20a3be" + ), + expected_public_key: hex!( + "d98a33fcb65f6ace1c6599c5895c8cee338d34f6fd21f883f306086e2e0af2bf" + ), + }, + // Mixed hardened and non-hardened derivation + Derivation { + path: &[2805853951, 2012627329, 3396580781, 1663824773], + expected_secret_key: hex!( + "0f0e4dfa88132151409f014584d112152dbd78c238afc1fa095cc852c49ffd46" + ), + expected_public_key: hex!( + "51b8f57fa35e2d95ed518dc9c0defcc7268e600781cb5d65f20f1e2898c92905" + ), + }, + Derivation { + path: &[3136119273, 140597163, 2240167577, 148040763], + expected_secret_key: hex!( + "08019f789391f195786891702464f7302e669c51f3e8af7c7f6f46f8d14b0182" + ), + expected_public_key: hex!( + "3b2cbb00f208011f4322a6c09020a437b1676e86e83f5d6953f94f3b1f0d4a39" + ), + }, + ], +}]; + +#[test] +fn test_vectors() { + for vector in TEST_VECTORS { + let mut root_sk = generic_ec::Scalar::::from_be_bytes( + &vector.root_secret_key, + ) + .expect("invalid root_sk"); + let root_sk = generic_ec::SecretScalar::new(&mut root_sk); + + let esk = hd_wallet::ExtendedSecretKey { + secret_key: root_sk, + chain_code: vector.chain_code, + }; + let ekey = hd_wallet::ExtendedKeyPair::from(esk); + + assert_eq!( + hex::encode(ekey.public_key().public_key.to_bytes(true)), + hex::encode(vector.root_public_key) + ); + + for derivation in vector.derivations { + eprintln!("path: {:?}", derivation.path); + let child_key = hd_wallet::Edwards::derive_child_key_pair_with_path( + &ekey, + derivation.path.iter().copied(), + ); + + assert_eq!( + hex::encode(child_key.secret_key().secret_key.as_ref().to_be_bytes()), + hex::encode(derivation.expected_secret_key) + ); + assert_eq!( + hex::encode(child_key.public_key().public_key.to_bytes(true)), + hex::encode(derivation.expected_public_key) + ); + } + } +}