#![warn(missing_docs)]
mod bip85;
#[cfg(feature = "hqc")]
mod kems;
mod keys;
mod mnemonic;
mod signatures;
mod slip10;
pub use bip32::secp256k1::ecdsa;
pub use bip85::{Bip85, Bip85Error};
#[cfg(feature = "hqc")]
pub use kems::{HhdKemSchemeError, KemSeed};
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 = "hqc")]
use crate::kem::{KemDecapsulationKey, KemEncapsulationKey, KemScheme};
#[cfg(feature = "mayo")]
use crate::mayo::{MayoSigningKey, MayoVerificationKey};
#[cfg(feature = "ml-dsa")]
use crate::ml_dsa::{MlDsaSigningKey, MlDsaVerificationKey};
use bip32::secp256k1::ecdsa::{SigningKey, VerifyingKey};
#[cfg(feature = "hqc")]
use keys::derive_hqc_keypair as derive_hqc_keypair_from_seed;
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>,
#[cfg(feature = "hqc")]
pub kem_master_seeds: HashMap<KemScheme, KemSeed>,
}
impl fmt::Debug for HHDWallet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("HHDWallet");
debug
.field("mnemonic", &"<redacted>")
.field("master_seeds", &self.master_seeds);
#[cfg(feature = "hqc")]
debug.field("kem_master_seeds", &self.kem_master_seeds);
debug.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,
#[cfg(feature = "hqc")]
kem_master_seeds: HashMap::new(),
})
}
pub fn new(schemes: Vec<SignatureScheme>, password: Option<&str>) -> Result<Self, WalletError> {
let mnemonic = Mnemonic::new_random();
Self::new_from_mnemonic(mnemonic, schemes, password)
}
#[cfg(feature = "hqc")]
pub fn new_from_mnemonic_with_kem_schemes(
mnemonic: Mnemonic,
signature_schemes: Vec<SignatureScheme>,
kem_schemes: Vec<KemScheme>,
password: Option<&str>,
) -> Result<Self, WalletError> {
let mut wallet = Self::new_from_mnemonic(mnemonic, signature_schemes, password)?;
for scheme in kem_schemes {
let child_seed =
Bip85::derive_kem_seed_from_mnemonic(&wallet.mnemonic, scheme, password)?;
wallet.kem_master_seeds.insert(scheme, child_seed);
}
Ok(wallet)
}
#[cfg(feature = "hqc")]
pub fn new_with_kem_schemes(
signature_schemes: Vec<SignatureScheme>,
kem_schemes: Vec<KemScheme>,
password: Option<&str>,
) -> Result<Self, WalletError> {
Self::new_from_mnemonic_with_kem_schemes(
Mnemonic::new_random(),
signature_schemes,
kem_schemes,
password,
)
}
pub fn mnemonic(&self) -> &Mnemonic {
&self.mnemonic
}
pub fn master_seeds(&self) -> &HashMap<SignatureScheme, SignatureSeed> {
&self.master_seeds
}
#[cfg(feature = "hqc")]
pub fn kem_master_seeds(&self) -> &HashMap<KemScheme, KemSeed> {
&self.kem_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)
}
#[cfg(feature = "hqc")]
fn derive_hqc_keypair(
&self,
scheme: KemScheme,
address_index: u32,
) -> Result<(KemEncapsulationKey, KemDecapsulationKey), WalletError> {
let kem_seed = self
.kem_master_seeds
.get(&scheme)
.ok_or(WalletError::KemSchemeNotConfigured(scheme))?;
derive_hqc_keypair_from_seed(scheme, kem_seed.as_seed().as_bytes(), address_index)
.map_err(WalletError::KeyError)
}
#[cfg(feature = "hqc")]
pub fn derive_hqc128_keypair(
&self,
address_index: u32,
) -> Result<(KemEncapsulationKey, KemDecapsulationKey), WalletError> {
self.derive_hqc_keypair(KemScheme::Hqc128, address_index)
}
#[cfg(feature = "hqc")]
pub fn derive_hqc192_keypair(
&self,
address_index: u32,
) -> Result<(KemEncapsulationKey, KemDecapsulationKey), WalletError> {
self.derive_hqc_keypair(KemScheme::Hqc192, address_index)
}
#[cfg(feature = "hqc")]
pub fn derive_hqc256_keypair(
&self,
address_index: u32,
) -> Result<(KemEncapsulationKey, KemDecapsulationKey), WalletError> {
self.derive_hqc_keypair(KemScheme::Hqc256, address_index)
}
}
#[derive(Debug, thiserror::Error)]
pub enum WalletError {
#[error("Invalid seed length: expected {expected}, got {actual}")]
InvalidSeedLength {
expected: usize,
actual: usize,
},
#[error("Invalid scheme")]
InvalidScheme,
#[cfg(feature = "hqc")]
#[error("KEM scheme '{0}' is not configured in this wallet")]
KemSchemeNotConfigured(KemScheme),
#[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("BIP-32 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(test, feature = "hqc"))]
#[allow(clippy::unwrap_used)]
mod hqc_tests {
use super::*;
use rstest::rstest;
fn fixed_mnemonic() -> Mnemonic {
Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
)
.unwrap()
}
fn derive(
wallet: &HHDWallet,
scheme: KemScheme,
address_index: u32,
) -> Result<(KemEncapsulationKey, KemDecapsulationKey), WalletError> {
if scheme == KemScheme::Hqc128 {
wallet.derive_hqc128_keypair(address_index)
} else if scheme == KemScheme::Hqc192 {
wallet.derive_hqc192_keypair(address_index)
} else {
wallet.derive_hqc256_keypair(address_index)
}
}
#[rstest]
#[case::hqc128(KemScheme::Hqc128)]
#[case::hqc192(KemScheme::Hqc192)]
#[case::hqc256(KemScheme::Hqc256)]
fn hqc_wallet_restoration_is_deterministic(#[case] scheme: KemScheme) {
let first = HHDWallet::new_from_mnemonic_with_kem_schemes(
fixed_mnemonic(),
Vec::new(),
vec![scheme],
None,
)
.unwrap();
let second = HHDWallet::new_from_mnemonic_with_kem_schemes(
fixed_mnemonic(),
Vec::new(),
vec![scheme],
None,
)
.unwrap();
let first_keys = derive(&first, scheme, 4).unwrap();
let second_keys = derive(&second, scheme, 4).unwrap();
assert_eq!(first_keys.0.to_raw_bytes(), second_keys.0.to_raw_bytes());
assert_eq!(first_keys.1.to_raw_bytes(), second_keys.1.to_raw_bytes());
}
#[test]
fn random_wallet_constructor_configures_requested_kem_seeds() {
let schemes = [KemScheme::Hqc128, KemScheme::Hqc192, KemScheme::Hqc256];
let wallet = HHDWallet::new_with_kem_schemes(Vec::new(), schemes.to_vec(), None).unwrap();
assert_eq!(wallet.kem_master_seeds().len(), schemes.len());
for scheme in schemes {
let seed = wallet
.kem_master_seeds()
.get(&scheme)
.expect("requested KEM seed should be configured");
assert_eq!(seed.scheme(), scheme);
}
}
#[test]
fn signature_only_constructor_does_not_implicitly_enable_hqc() {
let wallet = HHDWallet::new_from_mnemonic(fixed_mnemonic(), Vec::new(), None).unwrap();
assert!(matches!(
wallet.derive_hqc128_keypair(0),
Err(WalletError::KemSchemeNotConfigured(KemScheme::Hqc128))
));
}
#[cfg(all(feature = "encp", feature = "decp"))]
#[rstest]
#[case::hqc128(KemScheme::Hqc128)]
#[case::hqc192(KemScheme::Hqc192)]
#[case::hqc256(KemScheme::Hqc256)]
fn hhd_hqc_round_trip(#[case] scheme: KemScheme) {
let wallet = HHDWallet::new_from_mnemonic_with_kem_schemes(
fixed_mnemonic(),
Vec::new(),
vec![scheme],
None,
)
.unwrap();
let (encapsulation_key, decapsulation_key) = derive(&wallet, scheme, 0).unwrap();
let (ciphertext, sender_secret) = scheme.encapsulate(&encapsulation_key).unwrap();
let receiver_secret = scheme.decapsulate(&ciphertext, &decapsulation_key).unwrap();
assert_eq!(sender_secret.to_raw_bytes(), receiver_secret.to_raw_bytes());
}
}
#[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"));
}
}