use crate::hhd::signatures::SignatureScheme;
use bip32::DerivationPath;
use bip32::{ChildNumber, ExtendedKeyAttrs, KeyFingerprint, PrivateKey, PublicKey};
use hmac::{digest::crypto_common::InvalidLength, Hmac, Mac};
use sha2::Sha512;
use zeroize::Zeroize;
type HmacSha512 = Hmac<Sha512>;
const SLIP10_DERIVED_KEY_BYTES: usize = 32;
pub(crate) struct Slip10XPrvKey<K: PrivateKey> {
private_key: K,
attrs: ExtendedKeyAttrs,
}
impl<K: PrivateKey> std::fmt::Debug for Slip10XPrvKey<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Slip10XPrvKey")
.field("private_key", &"<redacted>")
.field("attrs", &self.attrs)
.finish()
}
}
impl<K: PrivateKey> Slip10XPrvKey<K> {
pub(crate) fn derive_child(
&self,
child_number: ChildNumber,
) -> Result<Slip10XPrvKey<K>, Slip10Error> {
let depth = self
.attrs
.depth
.checked_add(1)
.ok_or(Slip10Error::MaximumDerivationDepthExceeded)?;
let (tweak, chain_code) = self
.private_key
.derive_tweak(&self.attrs.chain_code, child_number)?;
let private_key = self.private_key.derive_child(tweak)?;
let attrs = ExtendedKeyAttrs {
parent_fingerprint: self.private_key.public_key().fingerprint(),
child_number,
chain_code,
depth,
};
Ok(Slip10XPrvKey { private_key, attrs })
}
pub(crate) fn private_key_bytes(&self) -> Vec<u8> {
self.private_key.to_bytes().into()
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Slip10;
impl Slip10 {
pub(crate) fn derive_from_path<S, K>(
seed: S,
path: &DerivationPath,
scheme: SignatureScheme,
) -> Result<Slip10XPrvKey<K>, Slip10Error>
where
S: AsRef<[u8]>,
K: PrivateKey,
{
validate_all_hardened(path)?;
path.iter().fold(
Self::derive_root_xprv_from_seed::<S, K>(seed, scheme),
|maybe_key, child_num| maybe_key.and_then(|key| key.derive_child(child_num)),
)
}
pub(crate) fn derive_root_xprv_from_seed<S, K>(
seed: S,
scheme: SignatureScheme,
) -> Result<Slip10XPrvKey<K>, Slip10Error>
where
S: AsRef<[u8]>,
K: PrivateKey,
{
if seed.as_ref().len() != scheme.root_seed_size() {
return Err(Slip10Error::InvalidSeedLength {
expected: scheme.root_seed_size(),
actual: seed.as_ref().len(),
});
}
let mut hmac = HmacSha512::new_from_slice(scheme.domain_separator())?;
hmac.update(seed.as_ref());
let mut result = hmac.finalize().into_bytes();
let (secret_key, chain_code) = result.split_at(SLIP10_DERIVED_KEY_BYTES);
let private_key = PrivateKey::from_bytes(secret_key.try_into()?)?;
let attrs = ExtendedKeyAttrs {
depth: 0,
parent_fingerprint: KeyFingerprint::default(),
child_number: ChildNumber::default(),
chain_code: chain_code.try_into()?,
};
result.zeroize();
Ok(Slip10XPrvKey { private_key, attrs })
}
}
pub(crate) fn validate_all_hardened(path: &DerivationPath) -> Result<(), Slip10Error> {
for child_num in path.iter() {
if !child_num.is_hardened() {
return Err(Slip10Error::InvalidDerivationPath(format!(
"Path component {} is not hardened. All path components must be hardened. Path: {}",
child_num, path
)));
}
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum Slip10Error {
#[error("Invalid derivation path: {0}")]
InvalidDerivationPath(String),
#[error("Invalid seed length: expected {expected}, got {actual}")]
InvalidSeedLength {
expected: usize,
actual: usize,
},
#[error("Invalid HMAC key length")]
InvalidHmacKeyLength(#[from] InvalidLength),
#[error("Array conversion failed: {0}")]
ConversionError(#[from] std::array::TryFromSliceError),
#[error("BIP32 error: {0}")]
Bip32(#[from] bip32::Error),
#[error("Maximum derivation depth exceeded")]
MaximumDerivationDepthExceeded,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::hhd::signatures::SignatureScheme;
use bip32::{secp256k1::ecdsa::SigningKey, DerivationPath, Prefix, Seed as Bip32Seed, XPrv};
const TEST_SEED_64: [u8; 64] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f,
];
#[test]
fn test_validate_all_hardened_valid_path() {
let path: DerivationPath = "m/44'/60'/0'/0'".parse().unwrap();
assert!(validate_all_hardened(&path).is_ok());
}
#[test]
fn test_validate_all_hardened_mixed_path() {
let path: DerivationPath = "m/44'/60'/0/0'".parse().unwrap(); assert!(validate_all_hardened(&path).is_err());
}
#[test]
fn test_derive_root_xprv_from_seed_valid_seed() {
let scheme = SignatureScheme::Falcon512;
let result = Slip10::derive_root_xprv_from_seed(TEST_SEED_64, scheme);
assert!(result.is_ok());
let key: Slip10XPrvKey<SigningKey> = result.unwrap();
assert_eq!(key.attrs.depth, 0);
assert_eq!(key.attrs.child_number, ChildNumber::default());
}
const TEST_VECTOR_3_SEED_HEX: &str = "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be";
const EXPECTED_ROOT_XPRV: &str = "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6";
const EXPECTED_M_0H_XPRV: &str = "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L";
#[test]
fn test_bip32_vector_3_hardened_paths() {
let scheme = SignatureScheme::EcdsaSecp256k1;
let seed_bytes = hex::decode(TEST_VECTOR_3_SEED_HEX).expect("should decode hex seed");
test_root_key_equivalence(&seed_bytes, scheme, EXPECTED_ROOT_XPRV);
test_path_equivalence(&seed_bytes, scheme, "m/0'", EXPECTED_M_0H_XPRV);
}
fn test_root_key_equivalence(seed: &[u8], scheme: SignatureScheme, expected_xprv_str: &str) {
let seed_64_array: [u8; 64] = seed.try_into().unwrap();
let bip32_seed = Bip32Seed::new(seed_64_array);
let bip32_root = XPrv::new(&bip32_seed).expect("should create BIP32 root");
let bip32_root_str = bip32_root.to_string(Prefix::XPRV);
assert_eq!(
bip32_root_str.as_str(),
expected_xprv_str,
"BIP-32 root should match test vector"
);
let slip10_root: Slip10XPrvKey<SigningKey> =
Slip10::derive_root_xprv_from_seed(seed_64_array, scheme)
.expect("should create SLIP10 root");
let bip32_private_bytes = bip32_root.to_bytes();
let slip10_private_bytes = slip10_root.private_key_bytes();
assert_eq!(
bip32_private_bytes.as_slice(),
slip10_private_bytes.as_slice(),
"Root private keys should match between BIP-32 and SLIP-0010"
);
assert_eq!(
bip32_root.attrs().chain_code,
slip10_root.attrs.chain_code.as_slice(),
"Root chain codes should match"
);
let bip32_public = bip32_root.public_key();
let slip10_public = slip10_root.private_key.public_key();
assert_eq!(
bip32_public.to_bytes().as_slice(),
slip10_public.to_bytes().as_slice(),
"Root public keys should match"
);
assert_eq!(bip32_root.attrs().depth, 0);
assert_eq!(slip10_root.attrs.depth, 0);
}
fn test_path_equivalence(
seed: &[u8],
scheme: SignatureScheme,
path_str: &str,
expected_xprv_str: &str,
) {
let seed_64_array: [u8; 64] = seed.try_into().unwrap();
let path: DerivationPath = path_str.parse().expect("should parse derivation path");
let bip32_seed = Bip32Seed::new(seed_64_array);
let bip32_root = XPrv::new(&bip32_seed).expect("should create BIP32 root");
let bip32_key = path
.iter()
.try_fold(bip32_root, |key, child_num| key.derive_child(child_num))
.expect("should derive BIP32 key");
let bip32_key_str = bip32_key.to_string(Prefix::XPRV);
assert_eq!(
bip32_key_str.as_str(),
expected_xprv_str,
"BIP-32 derived key should match test vector for path: {}",
path_str
);
let slip10_key: Slip10XPrvKey<SigningKey> =
Slip10::derive_from_path(seed_64_array, &path, scheme)
.expect("should derive SLIP10 key");
let bip32_private_bytes = bip32_key.to_bytes();
let slip10_private_bytes = slip10_key.private_key_bytes();
assert_eq!(
bip32_private_bytes.as_slice(),
slip10_private_bytes.as_slice(),
"Private keys should match between BIP-32 and SLIP-0010 for path: {}",
path_str
);
assert_eq!(
bip32_key.attrs().chain_code,
slip10_key.attrs.chain_code.as_slice(),
"Chain codes should match for path: {}",
path_str
);
let bip32_public = bip32_key.public_key();
let slip10_public = slip10_key.private_key.public_key();
assert_eq!(
bip32_public.to_bytes().as_slice(),
slip10_public.to_bytes().as_slice(),
"Public keys should match for path: {}",
path_str
);
assert_eq!(
bip32_key.attrs().depth,
slip10_key.attrs.depth,
"Depth should match for path: {}",
path_str
);
let last_child = path.iter().last();
if let Some(expected_child) = last_child {
assert_eq!(
bip32_key.attrs().child_number,
expected_child,
"Child number should match last path component"
);
assert_eq!(
slip10_key.attrs.child_number, expected_child,
"SLIP-0010 child number should match"
);
}
}
}