#![warn(missing_docs)]
mod bip85;
mod keys;
mod mnemonic;
mod signatures;
mod slip10;
pub use bip32::secp256k1::ecdsa;
pub use bip85::{Bip85, Bip85Error};
pub use keys::KeyError;
pub use mnemonic::{Mnemonic, MnemonicError};
pub use signatures::{SignatureScheme, SignatureSchemeError, SignatureSeed};
pub use slip10::Slip10Error;
#[cfg(feature = "falcon")]
use crate::falcon::{FalconSigningKey, FalconVerificationKey};
#[cfg(feature = "mayo")]
use crate::mayo::{MayoSigningKey, MayoVerificationKey};
#[cfg(feature = "ml-dsa")]
use crate::ml_dsa::{MlDsaSigningKey, MlDsaVerificationKey};
use bip32::secp256k1::ecdsa::{SigningKey, VerifyingKey};
use keys::EcdsaSecp256k1;
#[cfg(feature = "falcon")]
use keys::FnDsa512;
#[cfg(feature = "mayo")]
use keys::{Mayo1, Mayo2, Mayo3};
#[cfg(feature = "ml-dsa")]
use keys::{MlDsa44, MlDsa65, MlDsa87};
use std::{collections::HashMap, fmt};
pub struct HHDWallet {
pub mnemonic: Mnemonic,
pub master_seeds: HashMap<SignatureScheme, SignatureSeed>,
}
impl fmt::Debug for HHDWallet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HHDWallet")
.field("mnemonic", &"<redacted>")
.field("master_seeds", &self.master_seeds)
.finish()
}
}
impl HHDWallet {
pub fn new_from_mnemonic(
mnemonic: Mnemonic,
schemes: Vec<SignatureScheme>,
password: Option<&str>,
) -> Result<Self, WalletError> {
let mut master_seeds = HashMap::new();
for scheme in schemes {
let child_seed = Bip85::derive_seed_from_mnemonic(mnemonic.clone(), scheme, password)?;
master_seeds.insert(scheme, child_seed);
}
Ok(HHDWallet {
mnemonic,
master_seeds,
})
}
pub fn new(schemes: Vec<SignatureScheme>, password: Option<&str>) -> Result<Self, WalletError> {
let mnemonic = Mnemonic::new_random();
Self::new_from_mnemonic(mnemonic, schemes, password)
}
pub fn mnemonic(&self) -> &Mnemonic {
&self.mnemonic
}
pub fn master_seeds(&self) -> &HashMap<SignatureScheme, SignatureSeed> {
&self.master_seeds
}
pub fn derive_ecdsa_secp256k1_keypair(
&self,
address_index: u32,
) -> Result<(SigningKey, VerifyingKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::EcdsaSecp256k1)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
EcdsaSecp256k1::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "falcon")]
pub fn derive_fn_dsa512_keypair(
&self,
address_index: u32,
) -> Result<(FalconSigningKey, FalconVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::Falcon512)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
FnDsa512::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "ml-dsa")]
#[deprecated(since = "0.3.0", note = "use derive_mldsa65_keypair")]
pub fn derive_mldsa44_keypair(
&self,
address_index: u32,
) -> Result<(MlDsaSigningKey, MlDsaVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::MlDsa44)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
MlDsa44::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "ml-dsa")]
pub fn derive_mldsa65_keypair(
&self,
address_index: u32,
) -> Result<(MlDsaSigningKey, MlDsaVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::MlDsa65)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
MlDsa65::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "ml-dsa")]
pub fn derive_mldsa87_keypair(
&self,
address_index: u32,
) -> Result<(MlDsaSigningKey, MlDsaVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::MlDsa87)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
MlDsa87::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "mayo")]
pub fn derive_mayo1_keypair(
&self,
address_index: u32,
) -> Result<(MayoSigningKey, MayoVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::Mayo1)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
Mayo1::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "mayo")]
pub fn derive_mayo2_keypair(
&self,
address_index: u32,
) -> Result<(MayoSigningKey, MayoVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::Mayo2)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
Mayo2::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
#[cfg(feature = "mayo")]
pub fn derive_mayo3_keypair(
&self,
address_index: u32,
) -> Result<(MayoSigningKey, MayoVerificationKey), WalletError> {
let signature_seed = self
.master_seeds
.get(&SignatureScheme::Mayo3)
.ok_or(WalletError::InvalidScheme)?;
let seed_bytes = signature_seed.as_seed().as_bytes();
Mayo3::derive_from_seed(seed_bytes, address_index).map_err(WalletError::KeyError)
}
}
#[derive(Debug, thiserror::Error)]
pub enum WalletError {
#[error("Invalid seed length: expected {expected}, got {actual}")]
InvalidSeedLength {
expected: usize,
actual: usize,
},
#[error("Invalid scheme")]
InvalidScheme,
#[error("Invalid derivation path")]
InvalidDerivationPath,
#[error("Invalid HMAC key length: expected {expected}, got {actual}")]
InvalidHmacKeyLength {
expected: usize,
actual: usize,
},
#[error("Mnemonic error: {0}")]
Bip39(#[from] MnemonicError),
#[error("BIP32 error: {0}")]
Bip32(#[from] bip32::Error),
#[error("Signature scheme error: {0}")]
SignatureSchemeError(#[from] SignatureSchemeError),
#[error("Key error: {0}")]
KeyError(#[from] KeyError),
#[error("BIP85 error: {0}")]
Bip85Error(#[from] Bip85Error),
}
#[cfg(feature = "falcon")]
#[test]
fn mnemonic_determinism() {
let mnemonic = Mnemonic::new_random();
let schemes = vec![SignatureScheme::Falcon512];
let wallet1 = HHDWallet::new_from_mnemonic(mnemonic.clone(), schemes.clone(), None).unwrap();
let wallet2 = HHDWallet::new_from_mnemonic(mnemonic.clone(), schemes.clone(), None).unwrap();
let keypair1 = wallet1.derive_fn_dsa512_keypair(0).unwrap();
let keypair2 = wallet2.derive_fn_dsa512_keypair(0).unwrap();
assert_eq!(keypair1.0, keypair2.0);
assert_eq!(keypair1.1, keypair2.1);
}
#[cfg(all(feature = "sign", feature = "vrfy"))]
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[cfg(feature = "falcon")]
use crate::falcon::FalconScheme;
#[cfg(feature = "mayo")]
use crate::mayo::MayoScheme;
#[cfg(feature = "ml-dsa")]
use crate::ml_dsa::MlDsaScheme;
use bip32::secp256k1::ecdsa::{
signature::{Signer, Verifier},
Signature,
};
use rstest::rstest;
#[rstest]
#[cfg_attr(
all(feature = "ml-dsa", feature = "sign", feature = "vrfy"),
case::mldsa44(SignatureScheme::MlDsa44)
)]
#[cfg_attr(
all(feature = "ml-dsa", feature = "sign", feature = "vrfy"),
case::mldsa65(SignatureScheme::MlDsa65)
)]
#[cfg_attr(
all(feature = "ml-dsa", feature = "sign", feature = "vrfy"),
case::mldsa87(SignatureScheme::MlDsa87)
)]
#[cfg_attr(feature = "ml-dsa", case::mldsa87(SignatureScheme::MlDsa87))]
#[case::mldsa87(SignatureScheme::MlDsa87)]
fn test_hhd_wallet_sign_verify_with_schemes(#[case] scheme: SignatureScheme) {
let wallet = HHDWallet::new(vec![scheme], None).unwrap();
let message = b"Hello, world!";
let (sk, vk) = match scheme {
SignatureScheme::MlDsa44 => wallet.derive_mldsa44_keypair(0).unwrap(),
SignatureScheme::MlDsa65 => wallet.derive_mldsa65_keypair(0).unwrap(),
SignatureScheme::MlDsa87 => wallet.derive_mldsa87_keypair(0).unwrap(),
_ => panic!("Invalid scheme"),
};
let signature = match scheme {
SignatureScheme::MlDsa44 => MlDsaScheme::Dsa44.sign(message, &sk).unwrap(),
SignatureScheme::MlDsa65 => MlDsaScheme::Dsa65.sign(message, &sk).unwrap(),
SignatureScheme::MlDsa87 => MlDsaScheme::Dsa87.sign(message, &sk).unwrap(),
_ => panic!("Invalid scheme"),
};
let res = match scheme {
SignatureScheme::MlDsa44 => MlDsaScheme::Dsa44.verify(message, &signature, &vk),
SignatureScheme::MlDsa65 => MlDsaScheme::Dsa65.verify(message, &signature, &vk),
SignatureScheme::MlDsa87 => MlDsaScheme::Dsa87.verify(message, &signature, &vk),
_ => panic!("Invalid scheme"),
};
assert!(res.is_ok());
}
#[cfg(all(feature = "falcon", feature = "sign", feature = "vrfy"))]
#[test]
fn test_hhd_wallet_sign_verify_with_falcon() {
let wallet = HHDWallet::new(vec![SignatureScheme::Falcon512], None).unwrap();
let message = b"Hello, world!";
let (sk, vk) = wallet.derive_fn_dsa512_keypair(0).unwrap();
let signature = FalconScheme::Dsa512.sign(message, &sk).unwrap();
let res = FalconScheme::Dsa512.verify(message, &signature, &vk);
assert!(res.is_ok());
}
#[cfg(all(feature = "mayo", feature = "sign", feature = "vrfy"))]
#[rstest]
#[case::mayo1(SignatureScheme::Mayo1, MayoScheme::Mayo1)]
#[case::mayo2(SignatureScheme::Mayo2, MayoScheme::Mayo2)]
#[case::mayo3(SignatureScheme::Mayo3, MayoScheme::Mayo3)]
fn test_hhd_wallet_sign_verify_with_mayo(
#[case] scheme: SignatureScheme,
#[case] mayo_scheme: MayoScheme,
) {
let wallet = HHDWallet::new(vec![scheme], None).unwrap();
let message = b"Hello, world!";
let (sk, vk) = match scheme {
SignatureScheme::Mayo1 => wallet.derive_mayo1_keypair(0).unwrap(),
SignatureScheme::Mayo2 => wallet.derive_mayo2_keypair(0).unwrap(),
SignatureScheme::Mayo3 => wallet.derive_mayo3_keypair(0).unwrap(),
_ => panic!("Invalid scheme"),
};
let signature = mayo_scheme.sign(message, &sk).unwrap();
let res = mayo_scheme.verify(message, &signature, &vk);
assert!(res.is_ok());
}
#[test]
fn test_hhd_wallet_sign_verify_with_ecdsa() {
let wallet = HHDWallet::new(vec![SignatureScheme::EcdsaSecp256k1], None).unwrap();
let message = b"Hello, world!";
let (sk, vk) = wallet.derive_ecdsa_secp256k1_keypair(0).unwrap();
let signature: Signature = sk.sign(message);
let res = vk.verify(message, &signature);
assert!(res.is_ok());
}
#[test]
fn test_hhd_wallet_debug_display() {
let wallet = HHDWallet::new(
vec![SignatureScheme::EcdsaSecp256k1, SignatureScheme::Falcon512],
None,
)
.unwrap();
let debug_display = format!("{:?}", wallet);
println!("{}", debug_display);
assert!(debug_display.contains("mnemonic"));
assert!(debug_display.contains("master_seeds"));
assert!(debug_display.contains("ECDSAsecp256k1"));
assert!(debug_display.contains("Falcon512"));
}
}