feat(secret): add BIP-0032 secp256k1 derivation for Ethereum keys behind feature flag

Fix derive_ethereum_key to use BIP-0032 instead of SLIP-0010, which
incorrectly handled unhardened indices in the Ethereum path m/44'/60'/0'/0/0.
This commit is contained in:
2026-06-10 07:29:05 +00:00
parent 7bf0538416
commit f4cacdbcaf
4 changed files with 328 additions and 19 deletions

View File

@@ -73,6 +73,8 @@ pub enum SecretServiceError {
Encryption(String),
#[error("invalid path: {0}")]
InvalidPath(String),
#[error("unsupported key type")]
UnsupportedKeyType,
}
impl From<crate::mnemonic::MnemonicError> for SecretServiceError {
@@ -200,22 +202,35 @@ impl SecretServiceHandle {
}
/// Derive a secp256k1 (Ethereum) keypair at the given path.
///
/// Uses BIP-0032 derivation (HMAC-SHA512 with "Bitcoin seed") when the
/// `secp256k1` feature is enabled. Returns `UnsupportedKeyType` when the
/// feature is disabled.
pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, SecretServiceError> {
let inner = self.inner.read().unwrap();
if !inner.unlocked {
return Err(SecretServiceError::ServiceLocked);
}
let seed = inner
.seed
.as_ref()
.ok_or(SecretServiceError::ServiceLocked)?;
#[cfg(feature = "secp256k1")]
{
let inner = self.inner.read().unwrap();
if !inner.unlocked {
return Err(SecretServiceError::ServiceLocked);
}
let seed = inner
.seed
.as_ref()
.ok_or(SecretServiceError::ServiceLocked)?;
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
Ok(DerivedKey {
key_type: KeyType::Secp256k1,
private_key: key.private_key().to_vec(),
public_key: key.public_key().to_vec(),
})
let key = crate::ethereum::derive_secp256k1_path(seed.as_bytes(), path)?;
Ok(DerivedKey {
key_type: KeyType::Secp256k1,
private_key: key.private_key().to_vec(),
public_key: key.public_key().to_vec(),
})
}
#[cfg(not(feature = "secp256k1"))]
{
let _ = path;
Err(SecretServiceError::UnsupportedKeyType)
}
}
/// Encrypt plaintext using the derived encryption key.
@@ -399,4 +414,41 @@ mod tests {
service.lock();
assert!(service.decrypt(&encrypted).is_err());
}
#[cfg(feature = "secp256k1")]
#[test]
fn test_derive_ethereum_key_bip32() {
let service = SecretServiceHandle::new();
service.unlock_new(24).unwrap();
let key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
assert_eq!(key.key_type, KeyType::Secp256k1);
assert_eq!(key.private_key.len(), 32);
assert_eq!(key.public_key.len(), 33);
}
#[cfg(feature = "secp256k1")]
#[test]
fn test_ethereum_key_differs_from_ed25519() {
let service = SecretServiceHandle::new();
service.unlock_new(24).unwrap();
let eth_key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
let ed_key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
assert_ne!(eth_key.private_key, ed_key.private_key);
}
#[cfg(not(feature = "secp256k1"))]
#[test]
fn test_derive_ethereum_key_unsupported_without_feature() {
let service = SecretServiceHandle::new();
service.unlock_new(24).unwrap();
let result = service.derive_ethereum_key(PATHS::ETHEREUM);
assert!(matches!(
result,
Err(SecretServiceError::UnsupportedKeyType)
));
}
}