From 5295a7716c50786b9c8aac6227b45f316f810758 Mon Sep 17 00:00:00 2001 From: koushiro Date: Sun, 25 Jan 2026 12:27:19 +0800 Subject: [PATCH 1/4] feat(bip0032): add SLIP-0010 support with secp256k1/nist256p1/ed25519 - add slip10 traits, curve modules/backends, and feature flags - add SLIP-0010 docs and test vectors; split bip32/slip10 tests - update key helpers and curve error handling for new curves --- bip0032/Cargo.toml | 66 +++-- bip0032/README.md | 19 +- bip0032/SLIP-0010.md | 75 +++++ .../curve/ed25519/backends/ed25519_dalek.rs | 51 ++++ bip0032/src/curve/ed25519/backends/mod.rs | 17 ++ bip0032/src/curve/ed25519/mod.rs | 22 ++ bip0032/src/curve/error.rs | 67 +++++ bip0032/src/curve/mod.rs | 16 +- bip0032/src/curve/nist256p1/backends/mod.rs | 17 ++ bip0032/src/curve/nist256p1/backends/p256.rs | 89 ++++++ bip0032/src/curve/nist256p1/mod.rs | 22 ++ bip0032/src/curve/secp256k1/backends/k256.rs | 21 +- .../curve/secp256k1/backends/libsecp256k1.rs | 25 +- bip0032/src/curve/secp256k1/backends/mod.rs | 72 +---- .../src/curve/secp256k1/backends/secp256k1.rs | 27 +- bip0032/src/curve/secp256k1/mod.rs | 14 +- bip0032/src/curve/slip10.rs | 12 + bip0032/src/lib.rs | 32 ++- bip0032/src/xkey/core/mod.rs | 12 +- bip0032/src/xkey/core/private.rs | 81 +++--- bip0032/src/xkey/core/public.rs | 52 ++-- bip0032/src/xkey/mod.rs | 2 + bip0032/src/xkey/payload/mod.rs | 8 +- bip0032/src/xkey/slip10.rs | 270 ++++++++++++++++++ bip0032/tests/{vectors.rs => bip32.rs} | 0 bip0032/tests/slip10/common.rs | 81 ++++++ bip0032/tests/slip10/ed25519.rs | 116 ++++++++ bip0032/tests/slip10/nist256p1.rs | 166 +++++++++++ bip0032/tests/slip10/secp256k1.rs | 118 ++++++++ taplo.toml | 2 +- 30 files changed, 1379 insertions(+), 193 deletions(-) create mode 100644 bip0032/SLIP-0010.md create mode 100644 bip0032/src/curve/ed25519/backends/ed25519_dalek.rs create mode 100644 bip0032/src/curve/ed25519/backends/mod.rs create mode 100644 bip0032/src/curve/ed25519/mod.rs create mode 100644 bip0032/src/curve/error.rs create mode 100644 bip0032/src/curve/nist256p1/backends/mod.rs create mode 100644 bip0032/src/curve/nist256p1/backends/p256.rs create mode 100644 bip0032/src/curve/nist256p1/mod.rs create mode 100644 bip0032/src/curve/slip10.rs create mode 100644 bip0032/src/xkey/slip10.rs rename bip0032/tests/{vectors.rs => bip32.rs} (100%) create mode 100644 bip0032/tests/slip10/common.rs create mode 100644 bip0032/tests/slip10/ed25519.rs create mode 100644 bip0032/tests/slip10/nist256p1.rs create mode 100644 bip0032/tests/slip10/secp256k1.rs diff --git a/bip0032/Cargo.toml b/bip0032/Cargo.toml index c9c27e4..1333771 100644 --- a/bip0032/Cargo.toml +++ b/bip0032/Cargo.toml @@ -22,7 +22,7 @@ repository.workspace = true description = "Another Rust implementation of BIP-0032 standard" readme = "README.md" documentation = "https://docs.rs/bip0032" -keywords = ["bip32", "bitcoin", "crypto"] +keywords = ["bip32", "slip10", "bitcoin", "crypto"] categories = ["cryptography", "no-std"] [package.metadata.docs.rs] @@ -41,11 +41,20 @@ std = [ "k256?/std", "secp256k1?/std", "libsecp256k1?/std", + "p256?/std", + "ed25519-dalek?/std", ] + +# BIP-0032 (secp256k1) k256 = ["k256/arithmetic"] secp256k1 = ["dep:secp256k1"] libsecp256k1 = ["dep:libsecp256k1"] +# Optional SLIP-0010 extension (support secp256k1/nist256p1/ed25519 curve) +slip10 = [] +p256 = ["slip10", "p256/arithmetic"] +ed25519-dalek = ["slip10", "dep:ed25519-dalek"] + [dependencies] anyhow = { version = "1.0", default-features = false } bs58 = { version = "0.5", default-features = false, features = ["alloc", "check"] } @@ -54,27 +63,48 @@ ripemd = { version = "0.1", default-features = false } sha2 = { version = "0.10", default-features = false } zeroize = { version = "1.8", default-features = false } -# Different secp256k1 libraries +############################################################################### +# secp256k1 libraries +############################################################################### # https://github.com/RustCrypto/elliptic-curves/tree/master/k256 -[dependencies.k256] -default-features = false -optional = true -version = "0.13" - +k256 = { version = "0.13", default-features = false, features = ["alloc"], optional = true } # https://github.com/bitcoin-core/secp256k1 -[dependencies.secp256k1] -default-features = false -features = ["alloc"] -optional = true -version = "0.31" - +secp256k1 = { version = "0.31", default-features = false, features = ["alloc"], optional = true } # https://github.com/paritytech/libsecp256k1 # NOTE: libsecp256k1 crate is no longer maintained -[dependencies.libsecp256k1] -default-features = false -features = ["static-context"] -optional = true -version = "0.7" +libsecp256k1 = { version = "0.7", default-features = false, features = ["static-context"], optional = true } + +############################################################################### +# nist256p1 libraries (SLIP-0010 extension) +############################################################################### +# https://github.com/RustCrypto/elliptic-curves/tree/master/p256 +p256 = { version = "0.13", default-features = false, features = ["alloc"], optional = true } + +############################################################################### +# ed25519 libraries (SLIP-0010 extension) +############################################################################### +# https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek +ed25519-dalek = { version = "2.2.0", default-features = false, features = ["alloc"], optional = true } [dev-dependencies] const-hex = "1.12.0" + +[[test]] +name = "bip32" +path = "tests/bip32.rs" +required-features = ["k256"] + +[[test]] +name = "slip10-secp256k1" +path = "tests/slip10/secp256k1.rs" +required-features = ["slip10", "k256"] + +[[test]] +name = "slip10-nist256p1" +path = "tests/slip10/nist256p1.rs" +required-features = ["slip10", "p256"] + +[[test]] +name = "slip10-ed25519" +path = "tests/slip10/ed25519.rs" +required-features = ["slip10", "ed25519-dalek"] diff --git a/bip0032/README.md b/bip0032/README.md index 1bf2f65..fb52442 100644 --- a/bip0032/README.md +++ b/bip0032/README.md @@ -14,10 +14,16 @@ Another Rust implementation of [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard. +## Support curves and features + +| Curve | Feature | Backends | Hardened | Non-hardened (private) | Non-hardened (public) | Serialization | +| --------- | --------------------------------------- | ----------------------------- | -------- | ---------------------- | --------------------- | ------------- | +| secp256k1 | `k256` \| `secp256k1` \| `libsecp256k1` | k256, secp256k1, libsecp256k1 | yes | yes | yes | yes | + ## Usage -Seed material is typically derived from a BIP-0039 mnemonic (for example, via -[bip0039](https://crates.io/crates/bip0039)). +Seed material is typically derived from a [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic +(for example, via [bip0039](https://crates.io/crates/bip0039)). ```rust use bip0039::{Count, English, Mnemonic}; @@ -74,6 +80,11 @@ let xpub = child 4. Public parent key -> private child key: impossible (BIP-0032 does not allow it). +## SLIP-0010 (optional) + +[SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) support is available behind the `slip10` feature. +See [SLIP-0010.md](SLIP-0010.md) for details, examples, and the feature matrix. + ## Documentation See documentation and examples at https://docs.rs/bip0032. @@ -86,6 +97,10 @@ See documentation and examples at https://docs.rs/bip0032. - [`k256`](https://github.com/RustCrypto/elliptic-curves/tree/master/k256) (by default) - [`secp256k1`](https://github.com/rust-bitcoin/rust-secp256k1) - [`libsecp256k1`](https://github.com/paritytech/libsecp256k1) +- [x] Optional SLIP-0010 support + - secp256k1 ([compatible with BIP32](https://github.com/satoshilabs/slips/blob/master/slip-0010.md#compatibility-with-bip-0032)) + - NIST P-256 (a.k.a. secp256r1, prime256v1) ([`p256`](https://github.com/RustCrypto/elliptic-curves/tree/master/p256)) + - ed25519 ([`ed25519-dalek`](https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek)) - [x] Support `no_std` environment ## Performance diff --git a/bip0032/SLIP-0010.md b/bip0032/SLIP-0010.md new file mode 100644 index 0000000..81f64b8 --- /dev/null +++ b/bip0032/SLIP-0010.md @@ -0,0 +1,75 @@ +# SLIP-0010 + +This crate provides optional [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) support behind the `slip10` feature. +It shares the same [`ExtendedPrivateKey`]/[`ExtendedPublicKey`] structure as [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), +but the derivation rules and supported curves differ. + +## Supported curves and features + +| Curve | Feature | Backends | Hardened | Non-hardened (private) | Non-hardened (public) | Serialization | +| --------- | ---------------------------------------------------- | ----------------------------- | -------- | ---------------------- | --------------------- | ------------- | +| secp256k1 | `slip10` + (`k256` \| `secp256k1` \| `libsecp256k1`) | k256, secp256k1, libsecp256k1 | yes | yes | yes | no | +| nist256p1 | `slip10` + `p256` | p256 | yes | yes | yes | no | +| ed25519 | `slip10` + `ed25519-dalek` | ed25519-dalek | yes | no | no | no | + +Note: SLIP-0010 does not define a standardized extended key serialization. +Only the BIP32 secp256k1 encoding (xpub/xprv) is supported for serialization. + +## Usage + +Seed material is typically derived from a [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic +(for example, via [bip0039](https://crates.io/crates/bip0039)). + +```rust +use bip0039::{Count, English, Mnemonic}; + +let mnemonic = >::generate(Count::Words12); +let seed = mnemonic.to_seed(""); +``` + +The examples below assume the `seed` from above. + +### secp256k1 (non-hardened supported) + +```rust +use bip0032::{DerivationPath, ExtendedPrivateKey, curve::secp256k1::*}; +use bip0032::slip10::{Slip10MasterKey, Slip10NonHardenedDerivation}; + +# let seed = [0u8; 64]; +let master = ExtendedPrivateKey::>::new_slip10(&seed).unwrap(); +let path: DerivationPath = "m/0H/1".parse().unwrap(); +let child = Slip10NonHardenedDerivation::derive_slip10_path(&master, &path).unwrap(); +let public = child.public_key().to_bytes(); +``` + +### nist256p1 (non-hardened supported) + +```rust +use bip0032::{DerivationPath, ExtendedPrivateKey, curve::nist256p1::*}; +use bip0032::slip10::{Slip10MasterKey, Slip10NonHardenedDerivation}; + +# let seed = [0u8; 64]; +let master = ExtendedPrivateKey::>::new_slip10(&seed).unwrap(); +let path: DerivationPath = "m/0H/1".parse().unwrap(); +let child = Slip10NonHardenedDerivation::derive_slip10_path(&master, &path).unwrap(); +let public = child.public_key().to_bytes(); +``` + +### ed25519 (hardened only) + +```rust +use bip0032::{ExtendedPrivateKey, HardenedDerivationPath, curve::ed25519::*}; +use bip0032::slip10::{Slip10HardenedOnlyDerivation, Slip10MasterKey}; + +# let seed = [0u8; 64]; +let master = ExtendedPrivateKey::>::new_slip10(&seed).unwrap(); +let path: HardenedDerivationPath = "m/0H/1H".parse().unwrap(); +let child = Slip10HardenedOnlyDerivation::derive_slip10_path(&master, &path).unwrap(); +let public = child.public_key().to_bytes(); +``` + +## Notes + +- `Slip10NonHardenedDerivation` is implemented for both extended private and + extended public keys; hardened derivation is only available for private keys. +- Fingerprints are computed using Hash160 over the serialized public key bytes. diff --git a/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs b/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs new file mode 100644 index 0000000..5171b00 --- /dev/null +++ b/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs @@ -0,0 +1,51 @@ +use ed25519_dalek::{SigningKey, VerifyingKey}; + +use crate::curve::{CurveError, CurvePrivateKey, CurvePublicKey, ed25519::Ed25519Backend}; + +/// Ed25519 backend powered by the [`ed25519-dalek`](https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek) crate. +pub struct Ed25519DalekBackend; + +impl CurvePublicKey for VerifyingKey { + type Error = CurveError; + type Bytes = [u8; 33]; + + fn from_bytes(bytes: &Self::Bytes) -> Result { + if bytes[0] != 0 { + return Err(CurveError::from("invalid ed25519 public key prefix")); + } + + let mut raw = [0u8; 32]; + raw.copy_from_slice(&bytes[1..]); + VerifyingKey::from_bytes(&raw).map_err(CurveError::new) + } + + fn to_bytes(&self) -> Self::Bytes { + let raw = self.to_bytes(); + let mut out = [0u8; 33]; + out[1..].copy_from_slice(&raw); + out + } +} + +impl CurvePrivateKey for SigningKey { + type Error = CurveError; + type PublicKey = VerifyingKey; + type Bytes = [u8; 32]; + + fn from_bytes(bytes: &Self::Bytes) -> Result { + Ok(SigningKey::from_bytes(bytes)) + } + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes() + } + + fn to_public(&self) -> Self::PublicKey { + self.verifying_key() + } +} + +impl Ed25519Backend for Ed25519DalekBackend { + type PublicKey = VerifyingKey; + type PrivateKey = SigningKey; +} diff --git a/bip0032/src/curve/ed25519/backends/mod.rs b/bip0032/src/curve/ed25519/backends/mod.rs new file mode 100644 index 0000000..36818bd --- /dev/null +++ b/bip0032/src/curve/ed25519/backends/mod.rs @@ -0,0 +1,17 @@ +//! Backend implementations for Ed25519. + +use crate::curve::{CurvePrivateKey, CurvePublicKey}; + +/// Ed25519 backend interface. +pub trait Ed25519Backend { + /// Backend-specific public key type. + type PublicKey: CurvePublicKey; + /// Backend-specific private key type. + type PrivateKey: CurvePrivateKey; +} + +#[cfg(feature = "ed25519-dalek")] +mod ed25519_dalek; + +#[cfg(feature = "ed25519-dalek")] +pub use self::ed25519_dalek::Ed25519DalekBackend; diff --git a/bip0032/src/curve/ed25519/mod.rs b/bip0032/src/curve/ed25519/mod.rs new file mode 100644 index 0000000..7b8ac08 --- /dev/null +++ b/bip0032/src/curve/ed25519/mod.rs @@ -0,0 +1,22 @@ +//! Ed25519 curve implementation. + +use core::marker::PhantomData; + +use super::*; + +mod backends; +pub use self::backends::*; + +/// An Ed25519 curve parameterization for a specific backend. +pub struct Ed25519Curve(PhantomData); + +impl Curve for Ed25519Curve { + const HMAC_KEY: &'static [u8] = b"ed25519 seed"; + + type PublicKey = ::PublicKey; + type PrivateKey = ::PrivateKey; +} + +impl Slip10Curve for Ed25519Curve {} + +impl Slip10HardenedOnlyCurve for Ed25519Curve {} diff --git a/bip0032/src/curve/error.rs b/bip0032/src/curve/error.rs new file mode 100644 index 0000000..61acde9 --- /dev/null +++ b/bip0032/src/curve/error.rs @@ -0,0 +1,67 @@ +//! Curve-related error helpers. + +#[cfg(not(feature = "std"))] +use alloc::string::String; +use core::{error, fmt}; + +use crate::error::{ErrorSource, IntoErrorSource}; + +/// Common curve error type. +pub struct CurveError(ErrorSource); + +impl CurveError { + /// Creates a curve error from a source error. + #[cfg(feature = "std")] + pub fn new(error: E) -> Self + where + E: IntoErrorSource, + { + Self(error.into_error_source()) + } + + /// Creates a curve error from a source error. + #[cfg(not(feature = "std"))] + pub fn new(error: E) -> Self + where + E: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + let error = anyhow::Error::msg(error); + Self(error.into_error_source()) + } +} + +impl fmt::Debug for CurveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for CurveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl From for CurveError { + fn from(error: ErrorSource) -> Self { + Self(error) + } +} + +impl From for CurveError { + fn from(message: String) -> Self { + Self(ErrorSource::from(message)) + } +} + +impl From<&'static str> for CurveError { + fn from(message: &'static str) -> Self { + Self(ErrorSource::from(message)) + } +} + +impl error::Error for CurveError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(self.0.as_error()) + } +} diff --git a/bip0032/src/curve/mod.rs b/bip0032/src/curve/mod.rs index f51da68..57f8955 100644 --- a/bip0032/src/curve/mod.rs +++ b/bip0032/src/curve/mod.rs @@ -58,7 +58,19 @@ pub trait TweakableKey: Sized { fn add_tweak(&self, tweak: &[u8; 32]) -> Result; } -pub mod secp256k1; - /// Marker trait for BIP32-encodable curves. pub trait Bip32Curve: Curve {} + +mod error; +#[cfg(feature = "slip10")] +mod slip10; + +pub use self::error::CurveError; +#[cfg(feature = "slip10")] +pub use self::slip10::*; + +#[cfg(feature = "slip10")] +pub mod ed25519; +#[cfg(feature = "slip10")] +pub mod nist256p1; +pub mod secp256k1; diff --git a/bip0032/src/curve/nist256p1/backends/mod.rs b/bip0032/src/curve/nist256p1/backends/mod.rs new file mode 100644 index 0000000..16923cb --- /dev/null +++ b/bip0032/src/curve/nist256p1/backends/mod.rs @@ -0,0 +1,17 @@ +//! Backend implementations for NIST P-256. + +use crate::curve::{CurvePrivateKey, CurvePublicKey, TweakableKey}; + +/// NIST P-256 backend interface. +pub trait Nist256p1Backend { + /// Backend-specific public key type. + type PublicKey: CurvePublicKey + TweakableKey; + /// Backend-specific private key type. + type PrivateKey: CurvePrivateKey + TweakableKey; +} + +#[cfg(feature = "p256")] +mod p256; + +#[cfg(feature = "p256")] +pub use self::p256::P256Backend; diff --git a/bip0032/src/curve/nist256p1/backends/p256.rs b/bip0032/src/curve/nist256p1/backends/p256.rs new file mode 100644 index 0000000..702f5f6 --- /dev/null +++ b/bip0032/src/curve/nist256p1/backends/p256.rs @@ -0,0 +1,89 @@ +use p256::{ + AffinePoint, NonZeroScalar, ProjectivePoint, PublicKey, SecretKey, + elliptic_curve::sec1::ToEncodedPoint, +}; +use zeroize::Zeroizing; + +use crate::curve::{ + CurveError, CurvePrivateKey, CurvePublicKey, TweakableKey, nist256p1::Nist256p1Backend, +}; + +/// NIST P-256 backend powered by the [`p256`](https://github.com/RustCrypto/elliptic-curves/tree/master/p256) crate. +pub struct P256Backend; + +impl CurvePublicKey for PublicKey { + type Error = CurveError; + type Bytes = [u8; 33]; + + fn from_bytes(bytes: &Self::Bytes) -> Result { + PublicKey::from_sec1_bytes(bytes).map_err(CurveError::new) + } + + fn to_bytes(&self) -> Self::Bytes { + let encoded = self.to_encoded_point(true); + let mut out = [0u8; 33]; + out.copy_from_slice(encoded.as_bytes()); + out + } +} + +impl TweakableKey for PublicKey { + type Error = CurveError; + + fn add_tweak(&self, tweak: &[u8; 32]) -> Result { + let tweak_scalar = Zeroizing::new(nonzero_scalar_from_bytes(tweak)?); + let parent_point = self.to_projective(); + + let child_point = ProjectivePoint::GENERATOR * tweak_scalar.as_ref() + parent_point; + let child_affine = AffinePoint::from(child_point); + + PublicKey::from_affine(child_affine).map_err(CurveError::new) + } +} + +impl CurvePrivateKey for SecretKey { + type Error = CurveError; + type PublicKey = PublicKey; + type Bytes = [u8; 32]; + + fn from_bytes(bytes: &Self::Bytes) -> Result { + SecretKey::from_slice(bytes).map_err(CurveError::new) + } + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes().into() + } + + fn to_public(&self) -> Self::PublicKey { + self.public_key() + } + + fn zeroize(&mut self) { + // `p256::SecretKey` implements `ZeroizeOnDrop`, so `Drop` handles cleanup. + } +} + +impl TweakableKey for SecretKey { + type Error = CurveError; + + fn add_tweak(&self, tweak: &[u8; 32]) -> Result { + let tweak_scalar = Zeroizing::new(nonzero_scalar_from_bytes(tweak)?); + let key_scalar = Zeroizing::new(self.to_nonzero_scalar()); + + let child = tweak_scalar.as_ref() + key_scalar.as_ref(); + + SecretKey::from_bytes(&child.to_bytes()).map_err(CurveError::new) + } +} + +impl Nist256p1Backend for P256Backend { + type PublicKey = PublicKey; + type PrivateKey = SecretKey; +} + +fn nonzero_scalar_from_bytes(bytes: &[u8; 32]) -> Result { + let bytes = Zeroizing::new(*bytes); + let scalar = NonZeroScalar::from_repr((*bytes).into()); + + Option::::from(scalar).ok_or("invalid tweak scalar") +} diff --git a/bip0032/src/curve/nist256p1/mod.rs b/bip0032/src/curve/nist256p1/mod.rs new file mode 100644 index 0000000..e114eeb --- /dev/null +++ b/bip0032/src/curve/nist256p1/mod.rs @@ -0,0 +1,22 @@ +//! NIST P-256 curve implementation. + +use core::marker::PhantomData; + +use super::*; + +mod backends; +pub use self::backends::*; + +/// A NIST P-256 curve parameterization for a specific backend. +pub struct Nist256p1Curve(PhantomData); + +impl Curve for Nist256p1Curve { + const HMAC_KEY: &'static [u8] = b"Nist256p1 seed"; + + type PublicKey = ::PublicKey; + type PrivateKey = ::PrivateKey; +} + +impl Slip10Curve for Nist256p1Curve {} + +impl Slip10NonHardenedCurve for Nist256p1Curve {} diff --git a/bip0032/src/curve/secp256k1/backends/k256.rs b/bip0032/src/curve/secp256k1/backends/k256.rs index 1bc29a4..747928a 100644 --- a/bip0032/src/curve/secp256k1/backends/k256.rs +++ b/bip0032/src/curve/secp256k1/backends/k256.rs @@ -3,18 +3,19 @@ use k256::{ }; use zeroize::Zeroizing; -use super::BackendError; -use crate::curve::{CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend}; +use crate::curve::{ + CurveError, CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend, +}; /// Secp256k1 backend powered by the [`k256`](https://github.com/RustCrypto/elliptic-curves/tree/master/k256) crate. pub struct K256Backend; impl CurvePublicKey for PublicKey { - type Error = BackendError; + type Error = CurveError; type Bytes = [u8; 33]; fn from_bytes(bytes: &Self::Bytes) -> Result { - PublicKey::from_sec1_bytes(bytes).map_err(BackendError::new) + PublicKey::from_sec1_bytes(bytes).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -26,7 +27,7 @@ impl CurvePublicKey for PublicKey { } impl TweakableKey for PublicKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let tweak_scalar = Zeroizing::new(nonzero_scalar_from_bytes(tweak)?); @@ -35,17 +36,17 @@ impl TweakableKey for PublicKey { let child_point = ProjectivePoint::GENERATOR * tweak_scalar.as_ref() + parent_point; let child_affine = child_point.to_affine(); - PublicKey::from_affine(child_affine).map_err(BackendError::new) + PublicKey::from_affine(child_affine).map_err(CurveError::new) } } impl CurvePrivateKey for SecretKey { - type Error = BackendError; + type Error = CurveError; type PublicKey = PublicKey; type Bytes = [u8; 32]; fn from_bytes(bytes: &Self::Bytes) -> Result { - SecretKey::from_slice(bytes).map_err(BackendError::new) + SecretKey::from_slice(bytes).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -62,7 +63,7 @@ impl CurvePrivateKey for SecretKey { } impl TweakableKey for SecretKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let tweak_scalar = Zeroizing::new(nonzero_scalar_from_bytes(tweak)?); @@ -70,7 +71,7 @@ impl TweakableKey for SecretKey { let child = tweak_scalar.as_ref() + key_scalar.as_ref(); - SecretKey::from_bytes(&child.to_bytes()).map_err(BackendError::new) + SecretKey::from_bytes(&child.to_bytes()).map_err(CurveError::new) } } diff --git a/bip0032/src/curve/secp256k1/backends/libsecp256k1.rs b/bip0032/src/curve/secp256k1/backends/libsecp256k1.rs index 7680ac2..74e5b77 100644 --- a/bip0032/src/curve/secp256k1/backends/libsecp256k1.rs +++ b/bip0032/src/curve/secp256k1/backends/libsecp256k1.rs @@ -1,7 +1,8 @@ use libsecp256k1::{PublicKey, PublicKeyFormat, SecretKey}; -use super::BackendError; -use crate::curve::{CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend}; +use crate::curve::{ + CurveError, CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend, +}; /// Secp256k1 backend powered by the [`libsecp256k1`](https://github.com/paritytech/libsecp256k1) crate. /// @@ -12,8 +13,8 @@ pub struct Libsecp256k1Backend; struct SecretKeyGuard(SecretKey); impl SecretKeyGuard { - fn parse(bytes: &[u8; 32]) -> Result { - SecretKey::parse(bytes).map(Self).map_err(BackendError::new) + fn parse(bytes: &[u8; 32]) -> Result { + SecretKey::parse(bytes).map(Self).map_err(CurveError::new) } } @@ -30,11 +31,11 @@ impl Drop for SecretKeyGuard { } impl CurvePublicKey for PublicKey { - type Error = BackendError; + type Error = CurveError; type Bytes = [u8; 33]; fn from_bytes(bytes: &Self::Bytes) -> Result { - PublicKey::parse_slice(bytes, Some(PublicKeyFormat::Compressed)).map_err(BackendError::new) + PublicKey::parse_slice(bytes, Some(PublicKeyFormat::Compressed)).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -43,25 +44,25 @@ impl CurvePublicKey for PublicKey { } impl TweakableKey for PublicKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let tweak_key = SecretKeyGuard::parse(tweak)?; let mut out = *self; - out.tweak_add_assign(tweak_key.as_ref()).map_err(BackendError::new)?; + out.tweak_add_assign(tweak_key.as_ref()).map_err(CurveError::new)?; Ok(out) } } impl CurvePrivateKey for SecretKey { - type Error = BackendError; + type Error = CurveError; type PublicKey = PublicKey; type Bytes = [u8; 32]; fn from_bytes(bytes: &Self::Bytes) -> Result { - SecretKey::parse(bytes).map_err(BackendError::new) + SecretKey::parse(bytes).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -78,13 +79,13 @@ impl CurvePrivateKey for SecretKey { } impl TweakableKey for SecretKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let tweak_key = SecretKeyGuard::parse(tweak)?; let mut out = *self; - out.tweak_add_assign(tweak_key.as_ref()).map_err(BackendError::new)?; + out.tweak_add_assign(tweak_key.as_ref()).map_err(CurveError::new)?; Ok(out) } diff --git a/bip0032/src/curve/secp256k1/backends/mod.rs b/bip0032/src/curve/secp256k1/backends/mod.rs index 8a8da7a..b07c1f9 100644 --- a/bip0032/src/curve/secp256k1/backends/mod.rs +++ b/bip0032/src/curve/secp256k1/backends/mod.rs @@ -1,69 +1,13 @@ //! Backend implementations for secp256k1 curve. -#[cfg(not(feature = "std"))] -use alloc::string::String; -use core::{error, fmt}; - -use crate::error::{ErrorSource, IntoErrorSource}; - -/// Common backend error. -pub struct BackendError(ErrorSource); - -impl BackendError { - /// Creates a backend error from a source error. - #[cfg(feature = "std")] - pub fn new(error: E) -> Self - where - E: IntoErrorSource, - { - Self(error.into_error_source()) - } - - /// Creates a backend error from a source error. - #[cfg(not(feature = "std"))] - pub fn new(error: E) -> Self - where - E: fmt::Display + fmt::Debug + Send + Sync + 'static, - { - let error = anyhow::Error::msg(error); - Self(error.into_error_source()) - } -} - -impl fmt::Debug for BackendError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl fmt::Display for BackendError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl From for BackendError { - fn from(error: ErrorSource) -> Self { - Self(error) - } -} - -impl From for BackendError { - fn from(message: String) -> Self { - Self(ErrorSource::from(message)) - } -} - -impl From<&'static str> for BackendError { - fn from(message: &'static str) -> Self { - Self(ErrorSource::from(message)) - } -} - -impl error::Error for BackendError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - Some(self.0.as_error()) - } +use crate::curve::{CurvePrivateKey, CurvePublicKey, TweakableKey}; + +/// Secp256k1 backend interface. +pub trait Secp256k1Backend { + /// Backend-specific public key type. + type PublicKey: CurvePublicKey + TweakableKey; + /// Backend-specific private key type. + type PrivateKey: CurvePrivateKey + TweakableKey; } #[cfg(feature = "k256")] diff --git a/bip0032/src/curve/secp256k1/backends/secp256k1.rs b/bip0032/src/curve/secp256k1/backends/secp256k1.rs index f5f3399..757a799 100644 --- a/bip0032/src/curve/secp256k1/backends/secp256k1.rs +++ b/bip0032/src/curve/secp256k1/backends/secp256k1.rs @@ -1,8 +1,9 @@ -use ::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, SignOnly, VerifyOnly}; +use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, SignOnly, VerifyOnly}; use zeroize::Zeroizing; -use super::BackendError; -use crate::curve::{CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend}; +use crate::curve::{ + CurveError, CurvePrivateKey, CurvePublicKey, TweakableKey, secp256k1::Secp256k1Backend, +}; /// Secp256k1 FFI backend powered by the [`secp256k1`](https://github.com/rust-bitcoin/rust-secp256k1) crate. pub struct Secp256k1FfiBackend; @@ -11,9 +12,9 @@ pub struct Secp256k1FfiBackend; struct ScalarGuard(Scalar); impl ScalarGuard { - fn from_bytes(bytes: &[u8; 32]) -> Result { + fn from_bytes(bytes: &[u8; 32]) -> Result { let bytes = Zeroizing::new(*bytes); - Scalar::from_be_bytes(*bytes).map(Self).map_err(BackendError::new) + Scalar::from_be_bytes(*bytes).map(Self).map_err(CurveError::new) } } @@ -69,11 +70,11 @@ fn with_verification_context(f: impl FnOnce(&Secp256k1) -> R) -> } impl CurvePublicKey for PublicKey { - type Error = BackendError; + type Error = CurveError; type Bytes = [u8; 33]; fn from_bytes(bytes: &Self::Bytes) -> Result { - PublicKey::from_byte_array_compressed(*bytes).map_err(BackendError::new) + PublicKey::from_byte_array_compressed(*bytes).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -82,24 +83,24 @@ impl CurvePublicKey for PublicKey { } impl TweakableKey for PublicKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let scalar = ScalarGuard::from_bytes(tweak)?; with_verification_context(|secp| { - (*self).add_exp_tweak(secp, scalar.as_ref()).map_err(BackendError::new) + (*self).add_exp_tweak(secp, scalar.as_ref()).map_err(CurveError::new) }) } } impl CurvePrivateKey for SecretKey { - type Error = BackendError; + type Error = CurveError; type PublicKey = PublicKey; type Bytes = [u8; 32]; fn from_bytes(bytes: &Self::Bytes) -> Result { - SecretKey::from_byte_array(*bytes).map_err(BackendError::new) + SecretKey::from_byte_array(*bytes).map_err(CurveError::new) } fn to_bytes(&self) -> Self::Bytes { @@ -116,12 +117,12 @@ impl CurvePrivateKey for SecretKey { } impl TweakableKey for SecretKey { - type Error = BackendError; + type Error = CurveError; fn add_tweak(&self, tweak: &[u8; 32]) -> Result { let scalar = ScalarGuard::from_bytes(tweak)?; - (*self).add_tweak(scalar.as_ref()).map_err(BackendError::new) + (*self).add_tweak(scalar.as_ref()).map_err(CurveError::new) } } diff --git a/bip0032/src/curve/secp256k1/mod.rs b/bip0032/src/curve/secp256k1/mod.rs index 2799817..66a7a60 100644 --- a/bip0032/src/curve/secp256k1/mod.rs +++ b/bip0032/src/curve/secp256k1/mod.rs @@ -7,14 +7,6 @@ use super::*; mod backends; pub use self::backends::*; -/// Secp256k1 backend interface. -pub trait Secp256k1Backend { - /// Backend-specific public key type. - type PublicKey: CurvePublicKey + TweakableKey; - /// Backend-specific private key type. - type PrivateKey: CurvePrivateKey + TweakableKey; -} - /// A secp256k1 curve parameterization for a specific backend. pub struct Secp256k1Curve(PhantomData); @@ -26,3 +18,9 @@ impl Curve for Secp256k1Curve { } impl Bip32Curve for Secp256k1Curve {} + +#[cfg(feature = "slip10")] +impl Slip10Curve for Secp256k1Curve {} + +#[cfg(feature = "slip10")] +impl Slip10NonHardenedCurve for Secp256k1Curve {} diff --git a/bip0032/src/curve/slip10.rs b/bip0032/src/curve/slip10.rs new file mode 100644 index 0000000..05c9b4a --- /dev/null +++ b/bip0032/src/curve/slip10.rs @@ -0,0 +1,12 @@ +//! SLIP-10 curve marker traits. + +use super::Curve; + +/// Marker trait for SLIP-10 compatible curves. +pub trait Slip10Curve: Curve {} + +/// Marker trait for SLIP-10 curves that only allow hardened derivation. +pub trait Slip10HardenedOnlyCurve: Slip10Curve {} + +/// Marker trait for SLIP-10 curves that allow non-hardened derivation. +pub trait Slip10NonHardenedCurve: Slip10Curve {} diff --git a/bip0032/src/lib.rs b/bip0032/src/lib.rs index c52b176..881c856 100644 --- a/bip0032/src/lib.rs +++ b/bip0032/src/lib.rs @@ -1,6 +1,26 @@ +//! # bip0032 +//! +//! [![](https://github.com/koushiro/rust-bips/actions/workflows/bip0032.yml/badge.svg)][actions] +//! [![](https://img.shields.io/docsrs/bip0032)][docs.rs] +//! [![](https://img.shields.io/crates/v/bip0032)][crates.io] +//! [![](https://img.shields.io/crates/l/bip0032)][crates.io] +//! [![](https://img.shields.io/crates/d/bip0032.svg)][crates.io] +//! [![](https://img.shields.io/badge/MSRV-1.85.0-green?logo=rust)][whatrustisit] +//! +//! [actions]: https://github.com/koushiro/rust-bips/actions +//! [docs.rs]: https://docs.rs/bip0032 +//! [crates.io]: https://crates.io/crates/bip0032 +//! [whatrustisit]: https://www.whatrustisit.com +//! //! Another Rust implementation of [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard. //! -//! # Usage +//! ## Support curves and features +//! +//! | Curve | Feature | Backends | Hardened | Non-hardened (private) | Non-hardened (public) | Serialization | +//! | --- | --- | --- | --- | --- | --- | --- | +//! | secp256k1 | `k256` \| `secp256k1` \| `libsecp256k1` | k256, secp256k1, libsecp256k1 | yes | yes | yes | yes | +//! +//! ## Usage //! //! Seed material is typically derived from a BIP-0039 mnemonic (for example, via //! [bip0039](https://crates.io/crates/bip0039)). @@ -61,6 +81,12 @@ //! ``` //! //! 4. Public parent key -> private child key: impossible (BIP-0032 does not allow it). +//! +//! # SLIP-0010 (optional) +//! +//! SLIP-0010 support is available behind the `slip10` feature. It shares the same +//! `ExtendedPrivateKey`/`ExtendedPublicKey` types but uses SLIP-0010 derivation +//! rules. #![deny(unused_imports)] #![deny(missing_docs)] @@ -76,7 +102,9 @@ mod error; mod path; mod xkey; -pub use crate::{ +#[cfg(feature = "slip10")] +pub use self::xkey::slip10; +pub use self::{ error::*, path::{ChildNumber, DerivationPath, HardenedChildNumber, HardenedDerivationPath}, xkey::{ExtendedKeyPayload, ExtendedPrivateKey, ExtendedPublicKey, KnownVersion, Version}, diff --git a/bip0032/src/xkey/core/mod.rs b/bip0032/src/xkey/core/mod.rs index 5915ab4..0a0ac1a 100644 --- a/bip0032/src/xkey/core/mod.rs +++ b/bip0032/src/xkey/core/mod.rs @@ -13,7 +13,7 @@ pub use self::{private::ExtendedPrivateKey, public::ExtendedPublicKey}; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct ExtendedKeyMetadata { pub(crate) depth: u8, - pub(crate) parent_fingerprint: Option<[u8; 4]>, + pub(crate) parent_fingerprint: [u8; 4], pub(crate) child_number: u32, pub(crate) chain_code: [u8; 32], } @@ -21,7 +21,7 @@ pub(crate) struct ExtendedKeyMetadata { impl Zeroize for ExtendedKeyMetadata { fn zeroize(&mut self) { self.depth = 0; - self.parent_fingerprint = None; + self.parent_fingerprint = [0u8; 4]; self.child_number = 0; self.chain_code.zeroize(); } @@ -47,14 +47,18 @@ pub(crate) fn key_fingerprint(public_key_bytes: &[u8]) -> [u8; 4] { out } +pub(crate) fn derive_master_key_parts(seed: &[u8], domain: &[u8]) -> ([u8; 32], [u8; 32]) { + hmac_sha512_split(domain, |mac| mac.update(seed)) +} + pub(crate) fn hmac_sha512_split( key: &[u8], - update: impl FnOnce(&mut Hmac), + f: impl FnOnce(&mut Hmac), ) -> ([u8; 32], [u8; 32]) { let mut mac = Hmac::::new_from_slice(key) .expect("HMAC-SHA512 must accept the provided key length"); - update(&mut mac); + f(&mut mac); let output = mac.finalize().into_bytes(); debug_assert_eq!(output.len(), 64, "HMAC-SHA512 should produce a 64-byte output"); diff --git a/bip0032/src/xkey/core/private.rs b/bip0032/src/xkey/core/private.rs index 7c1e1e8..9198a1b 100644 --- a/bip0032/src/xkey/core/private.rs +++ b/bip0032/src/xkey/core/private.rs @@ -5,12 +5,12 @@ use core::str::FromStr; use hmac::Mac; use zeroize::Zeroizing; -use super::{ExtendedKeyMetadata, hmac_sha512_split, key_fingerprint, public::ExtendedPublicKey}; +use super::*; use crate::{ curve::{Bip32Curve, Curve, CurvePrivateKey, CurvePublicKey, TweakableKey}, error::{Error, ErrorKind, Result}, path::{ChildNumber, DerivationPath}, - xkey::{Version, payload::ExtendedKeyPayload}, + xkey::payload::*, }; /// A BIP32 extended private key. @@ -27,10 +27,6 @@ impl Clone for ExtendedPrivateKey { } } -fn derive_master_key_parts(seed: &[u8], domain: &[u8]) -> ([u8; 32], [u8; 32]) { - hmac_sha512_split(domain, |mac| mac.update(seed)) -} - impl ExtendedPrivateKey { /// Generates a master extended private key from a seed. pub fn new(seed: &[u8]) -> Result @@ -50,7 +46,7 @@ impl ExtendedPrivateKey { Ok(Self { meta: ExtendedKeyMetadata { depth: 0, - parent_fingerprint: Some([0u8; 4]), + parent_fingerprint: [0u8; 4], child_number: 0, chain_code, }, @@ -64,6 +60,30 @@ impl ExtendedPrivateKey { } } +impl ExtendedPrivateKey { + /// Returns the fingerprint of the parent's key. + pub fn parent_fingerprint(&self) -> [u8; 4] { + self.meta.parent_fingerprint + } + + /// Returns the chain code for this key. + pub fn chain_code(&self) -> [u8; 32] { + self.meta.chain_code + } + + /// Returns the private key bytes. + /// + /// # Warning + /// + /// Exposes raw private key material. Handle with care. + pub fn to_bytes(&self) -> Zeroizing<::Bytes> + where + ::Bytes: zeroize::Zeroize, + { + Zeroizing::new(CurvePrivateKey::to_bytes(&self.private_key)) + } +} + impl ExtendedPrivateKey where C: Bip32Curve, @@ -97,7 +117,7 @@ where Ok(Self { meta: ExtendedKeyMetadata { depth: self.meta.depth.saturating_add(1), - parent_fingerprint: Some(key_fingerprint(parent_public_bytes.as_ref())), + parent_fingerprint: key_fingerprint(parent_public_bytes.as_ref()), child_number: child.into(), chain_code: right, }, @@ -115,6 +135,13 @@ where } } +impl Drop for ExtendedPrivateKey { + fn drop(&mut self) { + CurvePrivateKey::zeroize(&mut self.private_key); + } +} + +// BIP32 encoding impl ExtendedPrivateKey where C: Bip32Curve, @@ -127,11 +154,6 @@ where .with_context("version", version)); } - if self.meta.parent_fingerprint.is_none() { - return Err(Error::new(ErrorKind::InvalidPayload, "missing parent fingerprint") - .with_context("depth", self.meta.depth)); - } - Ok(self.encode_with_unchecked(version)) } @@ -150,6 +172,20 @@ where } } +// BIP32 decoding +impl FromStr for ExtendedPrivateKey +where + C: Bip32Curve, + C::PrivateKey: CurvePrivateKey, +{ + type Err = Error; + + fn from_str(encoded: &str) -> Result { + let payload = encoded.parse::()?; + Self::try_from(payload) + } +} + impl TryFrom for ExtendedPrivateKey where C: Bip32Curve, @@ -175,22 +211,3 @@ where Ok(Self { meta: payload.meta.clone(), private_key }) } } - -impl FromStr for ExtendedPrivateKey -where - C: Bip32Curve, - C::PrivateKey: CurvePrivateKey, -{ - type Err = Error; - - fn from_str(encoded: &str) -> Result { - let payload = encoded.parse::()?; - Self::try_from(payload) - } -} - -impl Drop for ExtendedPrivateKey { - fn drop(&mut self) { - CurvePrivateKey::zeroize(&mut self.private_key); - } -} diff --git a/bip0032/src/xkey/core/public.rs b/bip0032/src/xkey/core/public.rs index a9cf182..5429ba7 100644 --- a/bip0032/src/xkey/core/public.rs +++ b/bip0032/src/xkey/core/public.rs @@ -27,6 +27,23 @@ impl Clone for ExtendedPublicKey { } } +impl ExtendedPublicKey { + /// Returns the fingerprint of the parent's key. + pub fn parent_fingerprint(&self) -> [u8; 4] { + self.meta.parent_fingerprint + } + + /// Returns the chain code for this key. + pub fn chain_code(&self) -> [u8; 32] { + self.meta.chain_code + } + + /// Returns the public key bytes. + pub fn to_bytes(&self) -> ::Bytes { + CurvePublicKey::to_bytes(&self.public_key) + } +} + impl ExtendedPublicKey where C: Bip32Curve, @@ -60,7 +77,7 @@ where Ok(Self { meta: ExtendedKeyMetadata { depth: self.meta.depth.saturating_add(1), - parent_fingerprint: Some(key_fingerprint(public_key_bytes.as_ref())), + parent_fingerprint: key_fingerprint(public_key_bytes.as_ref()), child_number: child.into(), chain_code: right, }, @@ -78,6 +95,7 @@ where } } +// BIP32 encoding impl ExtendedPublicKey where C: Bip32Curve, @@ -90,11 +108,6 @@ where .with_context("version", version)); } - if self.meta.parent_fingerprint.is_none() { - return Err(Error::new(ErrorKind::InvalidPayload, "missing parent fingerprint") - .with_context("depth", self.meta.depth)); - } - Ok(self.encode_with_unchecked(version)) } @@ -108,6 +121,20 @@ where } } +// BIP32 decoding +impl FromStr for ExtendedPublicKey +where + C: Bip32Curve, + C::PublicKey: CurvePublicKey, +{ + type Err = Error; + + fn from_str(encoded: &str) -> Result { + let payload = encoded.parse::()?; + Self::try_from(payload) + } +} + impl TryFrom for ExtendedPublicKey where C: Bip32Curve, @@ -131,16 +158,3 @@ where Ok(Self { meta: payload.meta.clone(), public_key }) } } - -impl FromStr for ExtendedPublicKey -where - C: Bip32Curve, - C::PublicKey: CurvePublicKey, -{ - type Err = Error; - - fn from_str(encoded: &str) -> Result { - let payload = encoded.parse::()?; - Self::try_from(payload) - } -} diff --git a/bip0032/src/xkey/mod.rs b/bip0032/src/xkey/mod.rs index be68934..cfa71d8 100644 --- a/bip0032/src/xkey/mod.rs +++ b/bip0032/src/xkey/mod.rs @@ -2,6 +2,8 @@ mod core; mod payload; +#[cfg(feature = "slip10")] +pub mod slip10; pub use self::{ core::{ExtendedPrivateKey, ExtendedPublicKey}, diff --git a/bip0032/src/xkey/payload/mod.rs b/bip0032/src/xkey/payload/mod.rs index 4cf8e54..8feb61a 100644 --- a/bip0032/src/xkey/payload/mod.rs +++ b/bip0032/src/xkey/payload/mod.rs @@ -48,11 +48,7 @@ impl ExtendedKeyPayload { let mut out = [0u8; Self::KEY_PAYLOAD_LENGTH]; out[..4].copy_from_slice(&self.version.to_bytes()); out[4] = self.meta.depth; - let parent_fingerprint = self - .meta - .parent_fingerprint - .expect("BIP32 serialization requires parent fingerprint"); - out[5..9].copy_from_slice(&parent_fingerprint); + out[5..9].copy_from_slice(&self.meta.parent_fingerprint); out[9..13].copy_from_slice(&self.meta.child_number.to_be_bytes()); out[13..45].copy_from_slice(&self.meta.chain_code); out[45..78].copy_from_slice(&self.key_data); @@ -197,7 +193,7 @@ pub(crate) fn parse_payload(data: &[u8]) -> Result { version, meta: ExtendedKeyMetadata { depth, - parent_fingerprint: Some(raw_parent_fingerprint), + parent_fingerprint: raw_parent_fingerprint, child_number, chain_code, }, diff --git a/bip0032/src/xkey/slip10.rs b/bip0032/src/xkey/slip10.rs new file mode 100644 index 0000000..7877cd1 --- /dev/null +++ b/bip0032/src/xkey/slip10.rs @@ -0,0 +1,270 @@ +#![doc = include_str!("../../SLIP-0010.md")] + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +use hmac::Mac; +use zeroize::Zeroizing; + +use crate::{ + curve::*, + error::{Error, ErrorKind, Result}, + path::{ChildNumber, DerivationPath, HardenedChildNumber, HardenedDerivationPath}, + xkey::core::*, +}; + +/// SLIP-0010 master key generation. +pub trait Slip10MasterKey { + /// Generates a SLIP-0010 master private key from a seed. + fn new_slip10(seed: &[u8]) -> Result + where + Self: Sized; +} + +/// Hardened-only SLIP-0010 derivation for private keys. +pub trait Slip10HardenedOnlyDerivation { + /// Derives a hardened child extended private key (SLIP-0010). + fn derive_slip10_child(&self, child: HardenedChildNumber) -> Result + where + Self: Sized; + + /// Derives a hardened child extended private key along a path (SLIP-0010). + fn derive_slip10_path(&self, path: &HardenedDerivationPath) -> Result + where + Self: Sized; +} + +/// Non-hardened SLIP-0010 derivation for public/private keys. +pub trait Slip10NonHardenedDerivation { + /// Derives a child extended public/private key (SLIP-0010). + fn derive_slip10_child(&self, child: ChildNumber) -> Result + where + Self: Sized; + + /// Derives a child extended public/private key along a path (SLIP-0010). + fn derive_slip10_path(&self, path: &DerivationPath) -> Result + where + Self: Sized; +} + +impl Slip10MasterKey for ExtendedPrivateKey +where + C: Slip10Curve, + C::PrivateKey: CurvePrivateKey, +{ + fn new_slip10(seed: &[u8]) -> Result { + let mut seed = seed.to_vec(); + + loop { + let (left, right) = derive_master_key_parts(&seed, C::HMAC_KEY); + let left = Zeroizing::new(left); + + match ::from_bytes(&*left) { + Ok(private_key) => { + return Ok(Self { + meta: ExtendedKeyMetadata { + depth: 0, + parent_fingerprint: [0u8; 4], + child_number: 0, + chain_code: right, + }, + private_key, + }); + }, + Err(err) => { + let mut next = [0u8; 64]; + next[..32].copy_from_slice(&*left); + next[32..].copy_from_slice(&right); + seed.clear(); + seed.extend_from_slice(&next); + if seed.is_empty() { + return Err(Error::new( + ErrorKind::InvalidDerivation, + "slip10 seed retry failed", + ) + .set_source(err)); + } + }, + } + } + } +} + +impl Slip10HardenedOnlyDerivation for ExtendedPrivateKey +where + C: Slip10HardenedOnlyCurve, + C::PrivateKey: CurvePrivateKey, +{ + fn derive_slip10_child(&self, child: HardenedChildNumber) -> Result { + let child_bytes = child.to_bytes(); + let parent_public = self.private_key.to_public(); + let parent_public_bytes = CurvePublicKey::to_bytes(&parent_public); + + let mut data = Zeroizing::new([0u8; 1 + 32 + 4]); + data[1..33].copy_from_slice(CurvePrivateKey::to_bytes(&self.private_key).as_ref()); + data[33..].copy_from_slice(&child_bytes); + + loop { + let (left, right) = hmac_sha512_split(&self.meta.chain_code, |mac| { + mac.update(data.as_ref()); + }); + + let left = Zeroizing::new(left); + let derived = ::from_bytes(&*left); + + match derived { + Ok(private_key) => { + return Ok(Self { + meta: ExtendedKeyMetadata { + depth: self.meta.depth.saturating_add(1), + parent_fingerprint: key_fingerprint(parent_public_bytes.as_ref()), + child_number: ChildNumber::from(child).into(), + chain_code: right, + }, + private_key, + }); + }, + Err(err) => { + let mut retry = Zeroizing::new([0u8; 1 + 32 + 4]); + retry[0] = 0x01; + retry[1..33].copy_from_slice(&right); + retry[33..].copy_from_slice(&child_bytes); + data = retry; + let _ = err; + }, + } + } + } + + fn derive_slip10_path(&self, path: &HardenedDerivationPath) -> Result { + let mut key = self.clone(); + for child in path.children() { + key = key.derive_slip10_child(child)?; + } + Ok(key) + } +} + +impl Slip10NonHardenedDerivation for ExtendedPrivateKey +where + C: Slip10NonHardenedCurve, + C::PrivateKey: CurvePrivateKey + TweakableKey, + C::PublicKey: CurvePublicKey, +{ + fn derive_slip10_child(&self, child: ChildNumber) -> Result { + let hardened = child.is_hardened(); + let parent_public = self.private_key.to_public(); + let parent_public_bytes = CurvePublicKey::to_bytes(&parent_public); + let child_bytes = child.to_bytes(); + + let mut data = Zeroizing::new([0u8; 1 + 32 + 4]); + if hardened { + data[1..33].copy_from_slice(CurvePrivateKey::to_bytes(&self.private_key).as_ref()); + data[33..].copy_from_slice(&child_bytes); + } else { + data[..33].copy_from_slice(parent_public_bytes.as_ref()); + data[33..].copy_from_slice(&child_bytes); + } + + loop { + let (left, right) = hmac_sha512_split(&self.meta.chain_code, |mac| { + mac.update(data.as_ref()); + }); + + let left = Zeroizing::new(left); + let derived = self.private_key.add_tweak(&left); + + match derived { + Ok(private_key) => { + return Ok(Self { + meta: ExtendedKeyMetadata { + depth: self.meta.depth.saturating_add(1), + parent_fingerprint: key_fingerprint(parent_public_bytes.as_ref()), + child_number: child.into(), + chain_code: right, + }, + private_key, + }); + }, + Err(err) => { + let mut retry = Zeroizing::new([0u8; 1 + 32 + 4]); + retry[0] = 0x01; + retry[1..33].copy_from_slice(&right); + retry[33..].copy_from_slice(&child_bytes); + data = retry; + let _ = err; + }, + } + } + } + + fn derive_slip10_path(&self, path: &DerivationPath) -> Result { + let mut key = self.clone(); + for child in path.children() { + key = key.derive_slip10_child(*child)?; + } + Ok(key) + } +} + +impl Slip10NonHardenedDerivation for ExtendedPublicKey +where + C: Slip10NonHardenedCurve, + C::PublicKey: CurvePublicKey + TweakableKey, +{ + fn derive_slip10_child(&self, child: ChildNumber) -> Result { + if child.is_hardened() { + return Err(Error::new( + ErrorKind::InvalidDerivation, + "cannot derive hardened child from public key", + ) + .with_context("child_index", child.index()) + .with_context("hardened", true)); + } + + let parent_public_bytes = CurvePublicKey::to_bytes(&self.public_key); + let child_bytes = child.to_bytes(); + let mut data = Zeroizing::new([0u8; 1 + 32 + 4]); + data[..33].copy_from_slice(parent_public_bytes.as_ref()); + data[33..].copy_from_slice(&child_bytes); + + loop { + let (left, right) = hmac_sha512_split(&self.meta.chain_code, |mac| { + mac.update(data.as_ref()); + }); + + let left = Zeroizing::new(left); + let derived = self.public_key.add_tweak(&left); + + match derived { + Ok(public_key) => { + return Ok(Self { + meta: ExtendedKeyMetadata { + depth: self.meta.depth.saturating_add(1), + parent_fingerprint: key_fingerprint(parent_public_bytes.as_ref()), + child_number: child.into(), + chain_code: right, + }, + public_key, + }); + }, + Err(err) => { + let mut retry = Zeroizing::new([0u8; 1 + 32 + 4]); + retry[0] = 0x01; + retry[1..33].copy_from_slice(&right); + retry[33..].copy_from_slice(&child_bytes); + data = retry; + let _ = err; + }, + } + } + } + + fn derive_slip10_path(&self, path: &DerivationPath) -> Result { + let mut key = self.clone(); + for child in path.children() { + key = key.derive_slip10_child(*child)?; + } + Ok(key) + } +} diff --git a/bip0032/tests/vectors.rs b/bip0032/tests/bip32.rs similarity index 100% rename from bip0032/tests/vectors.rs rename to bip0032/tests/bip32.rs diff --git a/bip0032/tests/slip10/common.rs b/bip0032/tests/slip10/common.rs new file mode 100644 index 0000000..762ef3f --- /dev/null +++ b/bip0032/tests/slip10/common.rs @@ -0,0 +1,81 @@ +#![cfg(feature = "slip10")] +#![allow(dead_code)] + +use bip0032::{DerivationPath, ExtendedPrivateKey, HardenedDerivationPath, curve::*, slip10::*}; + +pub struct Case { + pub path: &'static str, + pub fingerprint: &'static str, + pub chain_code: &'static str, + pub private: &'static str, + pub public: &'static str, +} + +pub fn decode_hex(value: &str) -> [u8; N] { + let bytes = const_hex::decode(value).expect("hex decode failed"); + assert_eq!(bytes.len(), N, "unexpected hex length for {value}"); + let mut out = [0u8; N]; + out.copy_from_slice(&bytes); + out +} + +pub fn assert_hardened_private_case(seed: &str, case: &Case) +where + C: Slip10HardenedOnlyCurve, + C::PrivateKey: CurvePrivateKey, + C::PublicKey: CurvePublicKey, +{ + let seed = const_hex::decode(seed).expect("seed hex decode failed"); + let master = as Slip10MasterKey>::new_slip10(&seed).unwrap(); + let path = case.path.parse::().unwrap(); + let derived = Slip10HardenedOnlyDerivation::derive_slip10_path(&master, &path).unwrap(); + + assert_eq!(derived.parent_fingerprint(), decode_hex(case.fingerprint)); + assert_eq!(derived.chain_code(), decode_hex(case.chain_code)); + + let private_bytes = derived.to_bytes(); + assert_eq!(private_bytes.as_ref(), &decode_hex::<32>(case.private)); + + let public_bytes = derived.public_key().to_bytes(); + assert_eq!(public_bytes, decode_hex(case.public)); +} + +pub fn assert_nonhardened_private_case(seed: &str, case: &Case) +where + C: Slip10NonHardenedCurve, + C::PrivateKey: CurvePrivateKey + TweakableKey, + C::PublicKey: CurvePublicKey + TweakableKey, +{ + let seed = const_hex::decode(seed).expect("seed hex decode failed"); + let master = as Slip10MasterKey>::new_slip10(&seed).unwrap(); + let path = case.path.parse::().unwrap(); + let derived = Slip10NonHardenedDerivation::derive_slip10_path(&master, &path).unwrap(); + + assert_eq!(derived.parent_fingerprint(), decode_hex(case.fingerprint)); + assert_eq!(derived.chain_code(), decode_hex(case.chain_code)); + + let private_bytes = derived.to_bytes(); + assert_eq!(private_bytes.as_ref(), &decode_hex::<32>(case.private)); + + let public_bytes = derived.public_key().to_bytes(); + assert_eq!(public_bytes, decode_hex(case.public)); +} + +pub fn assert_nonhardened_public_case(seed: &str, case: &Case) +where + C: Slip10NonHardenedCurve, + C::PrivateKey: CurvePrivateKey + TweakableKey, + C::PublicKey: CurvePublicKey + TweakableKey, +{ + let seed = const_hex::decode(seed).expect("seed hex decode failed"); + let master = as Slip10MasterKey>::new_slip10(&seed).unwrap(); + let master_public = master.public_key(); + let path = case.path.parse::().unwrap(); + let derived = Slip10NonHardenedDerivation::derive_slip10_path(&master_public, &path).unwrap(); + + assert_eq!(derived.parent_fingerprint(), decode_hex(case.fingerprint)); + assert_eq!(derived.chain_code(), decode_hex(case.chain_code)); + + let public_bytes = derived.to_bytes(); + assert_eq!(public_bytes, decode_hex(case.public)); +} diff --git a/bip0032/tests/slip10/ed25519.rs b/bip0032/tests/slip10/ed25519.rs new file mode 100644 index 0000000..dfed76f --- /dev/null +++ b/bip0032/tests/slip10/ed25519.rs @@ -0,0 +1,116 @@ +#![cfg(all(feature = "slip10", feature = "ed25519-dalek"))] + +use bip0032::curve::ed25519::{Ed25519Curve, Ed25519DalekBackend}; + +mod common; +use common::{Case, assert_hardened_private_case}; + +type Curve = Ed25519Curve; + +#[test] +fn slip10_ed25519_vector_1() { + let seed = "000102030405060708090a0b0c0d0e0f"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb", + private: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + public: "00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed", + }, + Case { + path: "m/0H", + fingerprint: "ddebc675", + chain_code: "8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69", + private: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", + public: "008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c", + }, + Case { + path: "m/0H/1H", + fingerprint: "13dab143", + chain_code: "a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14", + private: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", + public: "001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187", + }, + Case { + path: "m/0H/1H/2H", + fingerprint: "ebe4cb29", + chain_code: "2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c", + private: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", + public: "00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1", + }, + Case { + path: "m/0H/1H/2H/2H", + fingerprint: "316ec1c6", + chain_code: "8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc", + private: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", + public: "008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c", + }, + Case { + path: "m/0H/1H/2H/2H/1000000000H", + fingerprint: "d6322ccd", + chain_code: "68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230", + private: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", + public: "003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a", + }, + ]; + + for case in &cases { + assert_hardened_private_case::(seed, case); + } +} + +#[test] +fn slip10_ed25519_vector_2() { + let seed = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b", + private: "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", + public: "008fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a", + }, + Case { + path: "m/0H", + fingerprint: "31981b50", + chain_code: "0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d", + private: "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635", + public: "0086fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037", + }, + Case { + path: "m/0H/2147483647H", + fingerprint: "1e9411b1", + chain_code: "138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f", + private: "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4", + public: "005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d", + }, + Case { + path: "m/0H/2147483647H/1H", + fingerprint: "fcadf38c", + chain_code: "73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90", + private: "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c", + public: "002e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45", + }, + Case { + path: "m/0H/2147483647H/1H/2147483646H", + fingerprint: "aca70953", + chain_code: "0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a", + private: "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72", + public: "00e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b", + }, + Case { + path: "m/0H/2147483647H/1H/2147483646H/2H", + fingerprint: "422c654b", + chain_code: "5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4", + private: "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d", + public: "0047150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0", + }, + ]; + + for case in &cases { + assert_hardened_private_case::(seed, case); + } +} diff --git a/bip0032/tests/slip10/nist256p1.rs b/bip0032/tests/slip10/nist256p1.rs new file mode 100644 index 0000000..f353583 --- /dev/null +++ b/bip0032/tests/slip10/nist256p1.rs @@ -0,0 +1,166 @@ +#![cfg(all(feature = "slip10", feature = "p256"))] + +use bip0032::curve::nist256p1::{Nist256p1Curve, P256Backend}; + +mod common; +use common::{Case, assert_nonhardened_private_case, assert_nonhardened_public_case}; + +type Curve = Nist256p1Curve; + +#[test] +fn slip10_nist256p1_vector_1() { + let seed = "000102030405060708090a0b0c0d0e0f"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea", + private: "612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2", + public: "0266874dc6ade47b3ecd096745ca09bcd29638dd52c2c12117b11ed3e458cfa9e8", + }, + Case { + path: "m/0H", + fingerprint: "be6105b5", + chain_code: "3460cea53e6a6bb5fb391eeef3237ffd8724bf0a40e94943c98b83825342ee11", + private: "6939694369114c67917a182c59ddb8cafc3004e63ca5d3b84403ba8613debc0c", + public: "0384610f5ecffe8fda089363a41f56a5c7ffc1d81b59a612d0d649b2d22355590c", + }, + Case { + path: "m/0H/1", + fingerprint: "9b02312f", + chain_code: "4187afff1aafa8445010097fb99d23aee9f599450c7bd140b6826ac22ba21d0c", + private: "284e9d38d07d21e4e281b645089a94f4cf5a5a81369acf151a1c3a57f18b2129", + public: "03526c63f8d0b4bbbf9c80df553fe66742df4676b241dabefdef67733e070f6844", + }, + Case { + path: "m/0H/1/2H", + fingerprint: "b98005c1", + chain_code: "98c7514f562e64e74170cc3cf304ee1ce54d6b6da4f880f313e8204c2a185318", + private: "694596e8a54f252c960eb771a3c41e7e32496d03b954aeb90f61635b8e092aa7", + public: "0359cf160040778a4b14c5f4d7b76e327ccc8c4a6086dd9451b7482b5a4972dda0", + }, + Case { + path: "m/0H/1/2H/2", + fingerprint: "0e9f3274", + chain_code: "ba96f776a5c3907d7fd48bde5620ee374d4acfd540378476019eab70790c63a0", + private: "5996c37fd3dd2679039b23ed6f70b506c6b56b3cb5e424681fb0fa64caf82aaa", + public: "029f871f4cb9e1c97f9f4de9ccd0d4a2f2a171110c61178f84430062230833ff20", + }, + Case { + path: "m/0H/1/2H/2/1000000000", + fingerprint: "8b2b5c4b", + chain_code: "b9b7b82d326bb9cb5b5b121066feea4eb93d5241103c9e7a18aad40f1dde8059", + private: "21c4f269ef0a5fd1badf47eeacebeeaa3de22eb8e5b0adcd0f27dd99d34d0119", + public: "02216cd26d31147f72427a453c443ed2cde8a1e53c9cc44e5ddf739725413fe3f4", + }, + ]; + + for case in &cases { + assert_nonhardened_private_case::(seed, case); + } +} + +#[test] +fn slip10_nist256p1_vector_2_private_and_public() { + let seed = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "96cd4465a9644e31528eda3592aa35eb39a9527769ce1855beafc1b81055e75d", + private: "eaa31c2e46ca2962227cf21d73a7ef0ce8b31c756897521eb6c7b39796633357", + public: "02c9e16154474b3ed5b38218bb0463e008f89ee03e62d22fdcc8014beab25b48fa", + }, + Case { + path: "m/0", + fingerprint: "607f628f", + chain_code: "84e9c258bb8557a40e0d041115b376dd55eda99c0042ce29e81ebe4efed9b86a", + private: "d7d065f63a62624888500cdb4f88b6d59c2927fee9e6d0cdff9cad555884df6e", + public: "039b6df4bece7b6c81e2adfeea4bcf5c8c8a6e40ea7ffa3cf6e8494c61a1fc82cc", + }, + Case { + path: "m/0/2147483647H", + fingerprint: "946d2a54", + chain_code: "f235b2bc5c04606ca9c30027a84f353acf4e4683edbd11f635d0dcc1cd106ea6", + private: "96d2ec9316746a75e7793684ed01e3d51194d81a42a3276858a5b7376d4b94b9", + public: "02f89c5deb1cae4fedc9905f98ae6cbf6cbab120d8cb85d5bd9a91a72f4c068c76", + }, + Case { + path: "m/0/2147483647H/1", + fingerprint: "218182d8", + chain_code: "7c0b833106235e452eba79d2bdd58d4086e663bc8cc55e9773d2b5eeda313f3b", + private: "974f9096ea6873a915910e82b29d7c338542ccde39d2064d1cc228f371542bbc", + public: "03abe0ad54c97c1d654c1852dfdc32d6d3e487e75fa16f0fd6304b9ceae4220c64", + }, + Case { + path: "m/0/2147483647H/1/2147483646H", + fingerprint: "931223e4", + chain_code: "5794e616eadaf33413aa309318a26ee0fd5163b70466de7a4512fd4b1a5c9e6a", + private: "da29649bbfaff095cd43819eda9a7be74236539a29094cd8336b07ed8d4eff63", + public: "03cb8cb067d248691808cd6b5a5a06b48e34ebac4d965cba33e6dc46fe13d9b933", + }, + Case { + path: "m/0/2147483647H/1/2147483646H/2", + fingerprint: "956c4629", + chain_code: "3bfb29ee8ac4484f09db09c2079b520ea5616df7820f071a20320366fbe226a7", + private: "bb0a77ba01cc31d77205d51d08bd313b979a71ef4de9b062f8958297e746bd67", + public: "020ee02e18967237cf62672983b253ee62fa4dd431f8243bfeccdf39dbe181387f", + }, + ]; + + for case in &cases { + assert_nonhardened_private_case::(seed, case); + } + + assert_nonhardened_public_case::(seed, &cases[1]); +} + +#[test] +fn slip10_nist256p1_derivation_retry() { + let seed = "000102030405060708090a0b0c0d0e0f"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea", + private: "612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2", + public: "0266874dc6ade47b3ecd096745ca09bcd29638dd52c2c12117b11ed3e458cfa9e8", + }, + Case { + path: "m/28578H", + fingerprint: "be6105b5", + chain_code: "e94c8ebe30c2250a14713212f6449b20f3329105ea15b652ca5bdfc68f6c65c2", + private: "06f0db126f023755d0b8d86d4591718a5210dd8d024e3e14b6159d63f53aa669", + public: "02519b5554a4872e8c9c1c847115363051ec43e93400e030ba3c36b52a3e70a5b7", + }, + Case { + path: "m/28578H/33941", + fingerprint: "3e2b7bc6", + chain_code: "9e87fe95031f14736774cd82f25fd885065cb7c358c1edf813c72af535e83071", + private: "092154eed4af83e078ff9b84322015aefe5769e31270f62c3f66c33888335f3a", + public: "0235bfee614c0d5b2cae260000bb1d0d84b270099ad790022c1ae0b2e782efe120", + }, + ]; + + for case in &cases { + assert_nonhardened_private_case::(seed, case); + } +} + +#[test] +fn slip10_nist256p1_seed_retry() { + let seed = "a7305bc8df8d0951f0cb224c0e95d7707cbdf2c6ce7e8d481fec69c7ff5e9446"; + + let case = Case { + path: "m", + fingerprint: "00000000", + chain_code: "7762f9729fed06121fd13f326884c82f59aa95c57ac492ce8c9654e60efd130c", + private: "3b8c18469a4634517d6d0b65448f8e6c62091b45540a1743c5846be55d47d88f", + public: "0383619fadcde31063d8c5cb00dbfe1713f3e6fa169d8541a798752a1c1ca0cb20", + }; + + assert_nonhardened_private_case::(seed, &case); +} diff --git a/bip0032/tests/slip10/secp256k1.rs b/bip0032/tests/slip10/secp256k1.rs new file mode 100644 index 0000000..beb3898 --- /dev/null +++ b/bip0032/tests/slip10/secp256k1.rs @@ -0,0 +1,118 @@ +#![cfg(all(feature = "slip10", feature = "k256"))] + +use bip0032::curve::secp256k1::{K256Backend, Secp256k1Curve}; + +mod common; +use common::{Case, assert_nonhardened_private_case, assert_nonhardened_public_case}; + +type Curve = Secp256k1Curve; + +#[test] +fn slip10_secp256k1_vector_1() { + let seed = "000102030405060708090a0b0c0d0e0f"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", + private: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", + public: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2", + }, + Case { + path: "m/0H", + fingerprint: "3442193e", + chain_code: "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141", + private: "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea", + public: "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56", + }, + Case { + path: "m/0H/1", + fingerprint: "5c1bd648", + chain_code: "2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19", + private: "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368", + public: "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c", + }, + Case { + path: "m/0H/1/2H", + fingerprint: "bef5a2f9", + chain_code: "04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f", + private: "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca", + public: "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2", + }, + Case { + path: "m/0H/1/2H/2", + fingerprint: "ee7ab90c", + chain_code: "cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd", + private: "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4", + public: "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29", + }, + Case { + path: "m/0H/1/2H/2/1000000000", + fingerprint: "d880d7d8", + chain_code: "c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e", + private: "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8", + public: "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011", + }, + ]; + + for case in &cases { + assert_nonhardened_private_case::(seed, case); + } +} + +#[test] +fn slip10_secp256k1_vector_2() { + let seed = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"; + + let cases = [ + Case { + path: "m", + fingerprint: "00000000", + chain_code: "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689", + private: "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e", + public: "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7", + }, + Case { + path: "m/0", + fingerprint: "bd16bee5", + chain_code: "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c", + private: "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e", + public: "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea", + }, + Case { + path: "m/0/2147483647H", + fingerprint: "5a61ff8e", + chain_code: "be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9", + private: "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93", + public: "03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b", + }, + Case { + path: "m/0/2147483647H/1", + fingerprint: "d8ab4937", + chain_code: "f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb", + private: "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7", + public: "03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9", + }, + Case { + path: "m/0/2147483647H/1/2147483646H", + fingerprint: "78412e3a", + chain_code: "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29", + private: "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d", + public: "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0", + }, + Case { + path: "m/0/2147483647H/1/2147483646H/2", + fingerprint: "31a507b8", + chain_code: "9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271", + private: "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23", + public: "024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c", + }, + ]; + + for case in &cases { + assert_nonhardened_private_case::(seed, case); + } + + assert_nonhardened_public_case::(seed, &cases[1]); +} diff --git a/taplo.toml b/taplo.toml index b88288d..2084d8d 100644 --- a/taplo.toml +++ b/taplo.toml @@ -24,7 +24,7 @@ inline_table_expand = true compact_entries = false ## Target maximum column width after which arrays are expanded into new lines. ## Note that this is not set in stone, and works on a best-effort basis. -column_width = 100 # Default: 80 +column_width = 120 # Default: 80 ## Indent subtables if they come in order. indent_tables = false ## Indent entries under tables. From 9d333f0a0b8153db531f6a4a8ae7ddac7dbdfaca Mon Sep 17 00:00:00 2001 From: koushiro Date: Sun, 25 Jan 2026 12:36:43 +0800 Subject: [PATCH 2/4] fix --- bip0032/README.md | 2 +- bip0032/SLIP-0010.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bip0032/README.md b/bip0032/README.md index fb52442..49fe7f5 100644 --- a/bip0032/README.md +++ b/bip0032/README.md @@ -25,7 +25,7 @@ Another Rust implementation of [BIP-0032](https://github.com/bitcoin/bips/blob/m Seed material is typically derived from a [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic (for example, via [bip0039](https://crates.io/crates/bip0039)). -```rust +```rust,ignore use bip0039::{Count, English, Mnemonic}; let mnemonic = >::generate(Count::Words12); diff --git a/bip0032/SLIP-0010.md b/bip0032/SLIP-0010.md index 81f64b8..b883c13 100644 --- a/bip0032/SLIP-0010.md +++ b/bip0032/SLIP-0010.md @@ -20,7 +20,7 @@ Only the BIP32 secp256k1 encoding (xpub/xprv) is supported for serialization. Seed material is typically derived from a [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic (for example, via [bip0039](https://crates.io/crates/bip0039)). -```rust +```rust,ignore use bip0039::{Count, English, Mnemonic}; let mnemonic = >::generate(Count::Words12); From 40c74ae83d21c995af614d077669c0fb99ec54ff Mon Sep 17 00:00:00 2001 From: koushiro Date: Sun, 25 Jan 2026 12:41:25 +0800 Subject: [PATCH 3/4] update agents.md --- bip0032/AGENTS.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bip0032/AGENTS.md b/bip0032/AGENTS.md index 35a0caf..52f3c49 100644 --- a/bip0032/AGENTS.md +++ b/bip0032/AGENTS.md @@ -6,7 +6,12 @@ - `src/path/` defines derivation paths and child numbers. - `src/xkey/` contains extended key types and payload/version handling. - `src/curve/secp256k1/` holds backend-specific secp256k1 implementations (k256, secp256k1, libsecp256k1). -- Tests live in `tests/` (BIP-32 vectors and invalid key cases). +- `src/curve/nist256p1/` holds the NIST P-256 (p256) backend implementation for SLIP-0010. +- `src/curve/ed25519/` holds the ed25519 backend implementation for SLIP-0010. +- `src/curve/slip10.rs` contains SLIP-0010 marker traits. +- `src/xkey/slip10.rs` contains SLIP-0010 derivation implementations and includes docs from `SLIP-0010.md`. +- Tests live in `tests/` (`bip32.rs` for BIP-32 vectors and `slip10/*` for SLIP-0010 vectors). +- `SLIP-0010.md` documents SLIP-0010 usage and feature matrix. - Benchmarks are a workspace member under `benchmarks/`, with bench targets in `benchmarks/*.rs` and `benchmarks/serialize/`. - Auxiliary tooling: `benchmarks/` for benches and `fuzz/` for fuzz targets. @@ -15,7 +20,8 @@ - `cargo build` builds with default features (`std`, `k256`). - `cargo build --no-default-features` checks `no_std` compatibility. - `cargo test` runs unit tests for the enabled backend. -- `cargo test --features secp256k1` runs tests against specific backends. +- `cargo test --features k256` runs BIP-0032 tests against specific secp256k1 backends. +- `cargo test --features slip10,p256` runs SLIP-0010 tests for NIST P-256 curve (p256 backend). - `just bench keygen` or `just benches` runs benchmarks (uses `benchmarks/` as the working dir); equivalent: `cargo bench --bench keygen -- --quiet` from `benchmarks/`. - `just fuzz [runs]` runs fuzzing with nightly (`cargo +nightly fuzz`). - `just fuzz-clean` removes fuzz artifacts (`fuzz/artifacts`, `fuzz/corpus`). @@ -32,12 +38,13 @@ - Tests use fixed BIP-32 vectors; add similar deterministic cases for new behavior. - Run `cargo test` for default features; use `cargo test --all-features` when changing backend-related code. -- Prefer unit tests in `src/tests.rs` for API behavior and encoding/decoding edge cases. +- Use `tests/bip32.rs` for BIP-0032 vectors and `tests/slip10/*` for SLIP-0010 vectors. ## Feature Flags & Backends -- `std` enables standard library support; `k256` is the default backend. +- `std` enables standard library support; `k256` is the default secp256k1 backend. - Optional backends: `secp256k1` and `libsecp256k1` (note: libsecp256k1 is unmaintained). +- SLIP-0010 features: `slip10` (core), `k256`|`secp256k1`|`libsecp256k1` (secp256k1), `p256` (nist256p1), `ed25519-dalek` (ed25519). - When modifying backend code, validate the affected feature set with explicit `--features` flags. ## Commit & Pull Request Guidelines @@ -49,7 +56,7 @@ ## Security Notes - Do not log or print seeds, private keys, or derived secret material in tests or examples. -- Keep test data non-sensitive; use the published BIP-32 vectors in `src/tests.rs`. +- Keep test data non-sensitive; use the published BIP-0032 vectors in `tests/bip32.rs`; use the published SLIP-0010 vectors in `tests/slip10/*`. - Prefer `zeroize`-aware types and avoid cloning secret material unnecessarily. ## Release Notes From c12ff115d3e4cba7b540a1656cf6e45dc8e3861c Mon Sep 17 00:00:00 2001 From: koushiro Date: Sun, 25 Jan 2026 13:13:33 +0800 Subject: [PATCH 4/4] add ed25519_pubkey_from_slip10_bytes/ed25519_pubkey_to_slip10_bytes --- bip0032/SLIP-0010.md | 3 +++ .../curve/ed25519/backends/ed25519_dalek.rs | 2 +- bip0032/src/curve/ed25519/mod.rs | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bip0032/SLIP-0010.md b/bip0032/SLIP-0010.md index b883c13..a82f051 100644 --- a/bip0032/SLIP-0010.md +++ b/bip0032/SLIP-0010.md @@ -73,3 +73,6 @@ let public = child.public_key().to_bytes(); - `Slip10NonHardenedDerivation` is implemented for both extended private and extended public keys; hardened derivation is only available for private keys. - Fingerprints are computed using Hash160 over the serialized public key bytes. +- SLIP-0010 Ed25519 public keys are serialized as `0x00 || raw32`. Use + `ed25519_pubkey_from_slip10_bytes` / `ed25519_pubkey_to_slip10_bytes` + for conversion. diff --git a/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs b/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs index 5171b00..2324b57 100644 --- a/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs +++ b/bip0032/src/curve/ed25519/backends/ed25519_dalek.rs @@ -11,7 +11,7 @@ impl CurvePublicKey for VerifyingKey { fn from_bytes(bytes: &Self::Bytes) -> Result { if bytes[0] != 0 { - return Err(CurveError::from("invalid ed25519 public key prefix")); + return Err(CurveError::from("SLIP-0010 ed25519 public key needs a 0x00 prefix")); } let mut raw = [0u8; 32]; diff --git a/bip0032/src/curve/ed25519/mod.rs b/bip0032/src/curve/ed25519/mod.rs index 7b8ac08..2848d34 100644 --- a/bip0032/src/curve/ed25519/mod.rs +++ b/bip0032/src/curve/ed25519/mod.rs @@ -7,6 +7,24 @@ use super::*; mod backends; pub use self::backends::*; +/// Converts SLIP-0010 ed25519 public key bytes (0x00 || 32 bytes) into raw bytes. +pub fn ed25519_pubkey_from_slip10_bytes(bytes: &[u8; 33]) -> Result<[u8; 32], CurveError> { + if bytes[0] != 0x00 { + return Err(CurveError::from("SLIP-0010 ed25519 public key needs a 0x00 prefix")); + } + + let mut raw = [0u8; 32]; + raw.copy_from_slice(&bytes[1..]); + Ok(raw) +} + +/// Builds SLIP-0010 ed25519 public key bytes (0x00 || 32 bytes) from raw bytes. +pub fn ed25519_pubkey_to_slip10_bytes(raw: &[u8; 32]) -> [u8; 33] { + let mut out = [0u8; 33]; + out[1..].copy_from_slice(raw); + out +} + /// An Ed25519 curve parameterization for a specific backend. pub struct Ed25519Curve(PhantomData);