use crate::hhd::mnemonic::{Mnemonic, MnemonicError};
use crate::hhd::signatures::{SignatureScheme, SignatureSchemeError, SignatureSeed};
use bip32::{DerivationPath, Seed, XPrv};
use hmac::{Hmac, Mac};
use sha2::Sha512;
use zeroize::Zeroize;
type HmacSha512 = Hmac<Sha512>;
const BIP85_BASE_PATH: &str = "m/83696968'/83286642'";
const BIP85_KEY_INFO: &str = "bip-entropy-from-k";
#[derive(Clone, Copy, Debug)]
pub struct Bip85;
impl Bip85 {
pub fn child_index_from_scheme(scheme: SignatureScheme) -> u32 {
match scheme {
SignatureScheme::EcdsaSecp256k1 => 1,
SignatureScheme::Falcon512 => 2,
SignatureScheme::MlDsa44 => 4,
SignatureScheme::MlDsa65 => 5,
SignatureScheme::MlDsa87 => 6,
SignatureScheme::Mayo1 => 7,
SignatureScheme::Mayo2 => 8,
SignatureScheme::Mayo3 => 9,
}
}
pub fn child_path_from_scheme(scheme: SignatureScheme) -> String {
format!("{}'", Bip85::child_index_from_scheme(scheme))
}
pub fn derivation_path_from_scheme(scheme: SignatureScheme) -> String {
format!(
"{}/{}",
BIP85_BASE_PATH,
Bip85::child_path_from_scheme(scheme)
)
}
pub fn derivation_path_from_scheme_parsed(
scheme: SignatureScheme,
) -> Result<DerivationPath, SignatureSchemeError> {
Bip85::derivation_path_from_scheme(scheme)
.parse()
.map_err(SignatureSchemeError::InvalidDerivationPath)
}
pub fn derive_seed_from_mnemonic(
mnemonic: Mnemonic,
scheme: SignatureScheme,
password: Option<&str>,
) -> Result<SignatureSeed, Bip85Error> {
let master_seed = mnemonic.to_seed(password)?;
let scheme_derivation_path = Bip85::derivation_path_from_scheme_parsed(scheme)?;
let child_xprv = XPrv::derive_from_path(&master_seed, &scheme_derivation_path)?;
let mut private_key_bytes = child_xprv.to_bytes();
let seed = Bip85::extract_entropy(private_key_bytes.as_ref(), scheme)?;
private_key_bytes.zeroize();
Ok(seed)
}
pub fn extract_entropy(
private_key: &[u8],
scheme: SignatureScheme,
) -> Result<SignatureSeed, Bip85Error> {
let mut hmac = HmacSha512::new_from_slice(BIP85_KEY_INFO.as_bytes()).map_err(|_| {
Bip85Error::InvalidHmacKeyLength {
expected: 64, actual: BIP85_KEY_INFO.len(),
}
})?;
hmac.update(private_key.as_ref());
let mut child_entropy = hmac.finalize().into_bytes();
let mut child_seed: [u8; 64] = child_entropy.into();
let signature_seed = match scheme {
SignatureScheme::EcdsaSecp256k1 => SignatureSeed::ECDSAsecp256k1(Seed::new(child_seed)),
SignatureScheme::Falcon512 => SignatureSeed::Falcon512(Seed::new(child_seed)),
SignatureScheme::MlDsa44 => SignatureSeed::MlDsa44(Seed::new(child_seed)),
SignatureScheme::MlDsa65 => SignatureSeed::MlDsa65(Seed::new(child_seed)),
SignatureScheme::MlDsa87 => SignatureSeed::MlDsa87(Seed::new(child_seed)),
SignatureScheme::Mayo1 => SignatureSeed::Mayo1(Seed::new(child_seed)),
SignatureScheme::Mayo2 => SignatureSeed::Mayo2(Seed::new(child_seed)),
SignatureScheme::Mayo3 => SignatureSeed::Mayo3(Seed::new(child_seed)),
};
child_entropy.zeroize();
child_seed.zeroize();
Ok(signature_seed)
}
}
#[derive(Debug, thiserror::Error)]
pub enum Bip85Error {
#[error("Mnemonic error: {0}")]
Mnemonic(#[from] MnemonicError),
#[error("Invalid derivation path: {0}")]
InvalidDerivationPath(#[from] SignatureSchemeError),
#[error("Invalid HMAC key length: expected {expected}, got {actual}")]
InvalidHmacKeyLength {
expected: usize,
actual: usize,
},
#[error("BIP32 error: {0}")]
Bip32(#[from] bip32::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use bip32::XPrv;
use rstest::rstest;
#[rstest]
#[case(SignatureScheme::EcdsaSecp256k1, "m/83696968'/83286642'/1'")]
#[case(SignatureScheme::Falcon512, "m/83696968'/83286642'/2'")]
#[case(SignatureScheme::MlDsa65, "m/83696968'/83286642'/5'")]
#[case(SignatureScheme::MlDsa87, "m/83696968'/83286642'/6'")]
#[case(SignatureScheme::Mayo1, "m/83696968'/83286642'/7'")]
#[case(SignatureScheme::Mayo2, "m/83696968'/83286642'/8'")]
#[case(SignatureScheme::Mayo3, "m/83696968'/83286642'/9'")]
fn test_bip85_paths(#[case] scheme: SignatureScheme, #[case] expected: &str) {
assert_eq!(Bip85::derivation_path_from_scheme(scheme), expected);
}
#[rstest]
#[case(SignatureScheme::EcdsaSecp256k1, "m/83696968'/83286642'/1'")]
#[case(SignatureScheme::Falcon512, "m/83696968'/83286642'/2'")]
#[case(SignatureScheme::MlDsa65, "m/83696968'/83286642'/5'")]
#[case(SignatureScheme::MlDsa87, "m/83696968'/83286642'/6'")]
#[case(SignatureScheme::Mayo1, "m/83696968'/83286642'/7'")]
#[case(SignatureScheme::Mayo2, "m/83696968'/83286642'/8'")]
#[case(SignatureScheme::Mayo3, "m/83696968'/83286642'/9'")]
fn test_bip85_paths_parsed(#[case] scheme: SignatureScheme, #[case] expected: &str) {
let path =
Bip85::derivation_path_from_scheme_parsed(scheme).expect("should parse valid path");
assert_eq!(path.to_string(), expected);
}
fn test_bip_85(
master_xprv_str: &str,
derivation_path_str: &str,
expected_derived_key_hex: &str,
expected_derived_entropy_hex: &str,
) {
let root_key: XPrv = master_xprv_str.parse().expect("should parse valid xprv");
let derivation_path: DerivationPath = derivation_path_str
.parse()
.expect("should parse valid path");
let derived_xprv = derivation_path
.iter()
.try_fold(root_key, |key, child_num| key.derive_child(child_num))
.expect("should derive valid key");
let derived_key_bytes = derived_xprv.to_bytes();
let derived_key_hex: String = derived_key_bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert_eq!(derived_key_hex, expected_derived_key_hex);
let mut hmac =
HmacSha512::new_from_slice(BIP85_KEY_INFO.as_bytes()).expect("should create HMAC");
hmac.update(derived_key_bytes.as_ref());
let derived_entropy = hmac.finalize().into_bytes();
let derived_entropy_hex: String = derived_entropy
.iter()
.map(|b| format!("{:02x}", b))
.collect();
assert_eq!(derived_entropy_hex, expected_derived_entropy_hex);
}
#[test]
fn test_bip85_derivation_test_case_1() {
test_bip_85(
"xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb",
"m/83696968'/0'/0'",
"cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0",
"efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7",
);
}
#[test]
fn test_bip85_derivation_test_case_2() {
test_bip_85(
"xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb",
"m/83696968'/0'/1'",
"503776919131758bb7de7beb6c0ae24894f4ec042c26032890c29359216e21ba",
"70c6e3e8ebee8dc4c0dbba66076819bb8c09672527c4277ca8729532ad711872218f826919f6b67218adde99018a6df9095ab2b58d803b5b93ec9802085a690e",
);
}
#[test]
fn test_ecdsa_and_falcon_different_seeds_from_same_mnemonic() {
let mnemonic_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mnemonic = Mnemonic::from_phrase(mnemonic_phrase).expect("should parse valid mnemonic");
let password = None;
let ecdsa_seed = Bip85::derive_seed_from_mnemonic(
mnemonic.clone(),
SignatureScheme::EcdsaSecp256k1,
password,
)
.expect("should derive ECDSA seed");
let falcon_seed = Bip85::derive_seed_from_mnemonic(
mnemonic.clone(),
SignatureScheme::Falcon512,
password,
)
.expect("should derive Falcon seed");
let mldsa65_seed =
Bip85::derive_seed_from_mnemonic(mnemonic.clone(), SignatureScheme::MlDsa65, password)
.expect("should derive MlDsa65 seed");
let ecdsa_seed_bytes = ecdsa_seed.as_seed().as_bytes();
let falcon_seed_bytes = falcon_seed.as_seed().as_bytes();
let mldsa65_seed_bytes = mldsa65_seed.as_seed().as_bytes();
assert_ne!(
ecdsa_seed_bytes, falcon_seed_bytes,
"ECDSA and Falcon seeds should be different from the same mnemonic"
);
assert_ne!(
ecdsa_seed_bytes, mldsa65_seed_bytes,
"ECDSA and MlDsa65 seeds should be different from the same mnemonic"
);
assert_ne!(
falcon_seed_bytes, mldsa65_seed_bytes,
"Falcon and MlDsa65 seeds should be different from the same mnemonic"
);
assert_eq!(
Bip85::derivation_path_from_scheme(SignatureScheme::EcdsaSecp256k1),
"m/83696968'/83286642'/1'",
"ECDSA should use derivation path ending in 1'"
);
assert_eq!(
Bip85::derivation_path_from_scheme(SignatureScheme::Falcon512),
"m/83696968'/83286642'/2'",
"Falcon should use derivation path ending in 2'"
);
}
}