diff --git a/.changes/typesafer-slip10.md b/.changes/typesafer-slip10.md new file mode 100644 index 00000000..ef46c5ae --- /dev/null +++ b/.changes/typesafer-slip10.md @@ -0,0 +1,5 @@ +--- +"iota-crypto": minor +--- + +More static type safety for SLIP10 implementation to avoid runtime checks. diff --git a/src/error.rs b/src/error.rs index a813cb4b..44099e25 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,6 +31,9 @@ pub enum Error { PrivateKeyError, /// InvalidArgumentError InvalidArgumentError { alg: &'static str, expected: &'static str }, + #[cfg(feature = "slip10")] + /// Slip10 Segment Hardening Error + Slip10Error(crate::keys::slip10::SegmentHardeningError), /// System Error SystemError { call: &'static str, @@ -53,6 +56,8 @@ impl Display for Error { Error::ConvertError { from, to } => write!(f, "failed to convert {} to {}", from, to), Error::PrivateKeyError => write!(f, "Failed to generate private key."), Error::InvalidArgumentError { alg, expected } => write!(f, "{} expects {}", alg, expected), + #[cfg(feature = "slip10")] + Error::Slip10Error(inner) => write!(f, "slip10 error: {inner:?}"), Error::SystemError { call, raw_os_error: None, diff --git a/src/keys/slip10.rs b/src/keys/slip10.rs index ff9faca0..242c95eb 100644 --- a/src/keys/slip10.rs +++ b/src/keys/slip10.rs @@ -15,34 +15,54 @@ use crate::macs::hmac::HMAC_SHA512; // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki // https://en.bitcoin.it/wiki/BIP_0039 +/// The traits in hazmat module are implementation internals. +/// The traits are made public due to other public API requiring them. +/// The traits are not exported to prevent third parties from implementing them outside this crate. This prevents third +/// parties from importing and using them in polymorphic contexts. mod hazmat { - pub trait Derivable { + use super::Segment; + + /// Prevent external crates from deriving hazmat traits. + pub trait Sealed {} + /// Derivable secret and public keys. + pub trait Derivable: Sealed { fn is_key_valid(key_bytes: &[u8; 33]) -> bool; fn to_key(key_bytes: &[u8; 33]) -> Self; fn add_key(key_bytes: &mut [u8; 33], parent_key: &[u8; 33]) -> bool; - - const ALLOW_NON_HARDENED: bool; - const ALLOW_HARDENED: bool; - fn calc_non_hardened_data(key_bytes: &[u8; 33]) -> [u8; 33]; } + /// Derivable secret key. pub trait IsSecretKey: Derivable { const SEEDKEY: &'static [u8]; - // PublicKey type may not be Derivable as is the case with ed25519 + /// Type of corresponding public key; PublicKey type may not be Derivable as is the case with ed25519. type PublicKey; } + /// Derivable public key. pub trait IsPublicKey: Derivable { + /// Corresponding derivable secret key type type SecretKey: IsSecretKey; } + /// Derivable secret key whose corresponding public key is also derivable. + // The trait should have been defined as `trait ToPublic: IsSecretKey where Self::PublicKey: IsPublicKey`. It makes generic arguments more complex and seems like overkill. + pub trait ToPublic: IsSecretKey { + fn to_public(sk_bytes: &[u8; 33]) -> [u8; 33]; + } + /// Keys that can be used to compute "data" argument of SLIP10 derivation algorithm for a specific segment type. + pub trait CalcData: Sealed { + fn calc_data(key_bytes: &[u8; 33], segment: S) -> [u8; 33]; + } } +pub use hazmat::{CalcData, Derivable, IsPublicKey, IsSecretKey, ToPublic}; + #[cfg(feature = "ed25519")] pub mod ed25519 { - use super::hazmat::*; + use super::{hazmat::*, Hardened}; use crate::signatures::ed25519; + impl Sealed for ed25519::SecretKey {} + impl Derivable for ed25519::SecretKey { - const ALLOW_NON_HARDENED: bool = false; - const ALLOW_HARDENED: bool = true; fn is_key_valid(key_bytes: &[u8; 33]) -> bool { key_bytes[0] == 0 } @@ -54,9 +74,6 @@ pub mod ed25519 { fn add_key(_key_bytes: &mut [u8; 33], _parent_key: &[u8; 33]) -> bool { true } - fn calc_non_hardened_data(_key_bytes: &[u8; 33]) -> [u8; 33] { - panic!("SLIP10: ed25519 does not support non-hardened key derivation") - } } impl IsSecretKey for ed25519::SecretKey { @@ -64,17 +81,21 @@ pub mod ed25519 { type PublicKey = ed25519::PublicKey; } - pub type ExtendedSecretKey = super::Slip10; + impl CalcData for ed25519::SecretKey { + fn calc_data(key_bytes: &[u8; 33], _segment: Hardened) -> [u8; 33] { + *key_bytes + } + } } #[cfg(feature = "secp256k1")] pub mod secp256k1 { - use super::hazmat::*; + use super::{hazmat::*, Hardened, NonHardened, Segment}; use crate::signatures::secp256k1_ecdsa; + impl Sealed for secp256k1_ecdsa::SecretKey {} + impl Derivable for secp256k1_ecdsa::SecretKey { - const ALLOW_NON_HARDENED: bool = true; - const ALLOW_HARDENED: bool = true; fn is_key_valid(key_bytes: &[u8; 33]) -> bool { debug_assert_eq!(0, key_bytes[0]); let sk_bytes: &[u8; 32] = unsafe { &*(key_bytes[1..].as_ptr() as *const [u8; 32]) }; @@ -108,7 +129,15 @@ pub mod secp256k1 { false } } - fn calc_non_hardened_data(key_bytes: &[u8; 33]) -> [u8; 33] { + } + + impl IsSecretKey for secp256k1_ecdsa::SecretKey { + const SEEDKEY: &'static [u8] = b"Bitcoin seed"; + type PublicKey = secp256k1_ecdsa::PublicKey; + } + + impl ToPublic for secp256k1_ecdsa::SecretKey { + fn to_public(key_bytes: &[u8; 33]) -> [u8; 33] { use k256::elliptic_curve::sec1::ToEncodedPoint; debug_assert_eq!(0, key_bytes[0]); let sk_bytes: &[u8; 32] = unsafe { &*(key_bytes[1..].as_ptr() as *const [u8; 32]) }; @@ -120,14 +149,31 @@ pub mod secp256k1 { } } - impl IsSecretKey for secp256k1_ecdsa::SecretKey { - const SEEDKEY: &'static [u8] = b"Bitcoin seed"; - type PublicKey = secp256k1_ecdsa::PublicKey; + impl CalcData for secp256k1_ecdsa::SecretKey { + fn calc_data(key_bytes: &[u8; 33], _segment: Hardened) -> [u8; 33] { + *key_bytes + } } + impl CalcData for secp256k1_ecdsa::SecretKey { + fn calc_data(key_bytes: &[u8; 33], _segment: NonHardened) -> [u8; 33] { + Self::to_public(key_bytes) + } + } + + impl CalcData for secp256k1_ecdsa::SecretKey { + fn calc_data(key_bytes: &[u8; 33], segment: u32) -> [u8; 33] { + if segment.is_hardened() { + Self::calc_data(key_bytes, Hardened(segment)) + } else { + Self::calc_data(key_bytes, NonHardened(segment)) + } + } + } + + impl Sealed for secp256k1_ecdsa::PublicKey {} + impl Derivable for secp256k1_ecdsa::PublicKey { - const ALLOW_NON_HARDENED: bool = true; - const ALLOW_HARDENED: bool = false; fn is_key_valid(key_bytes: &[u8; 33]) -> bool { (key_bytes[0] == 2 || key_bytes[0] == 3) && k256::PublicKey::from_sec1_bytes(key_bytes).is_ok() } @@ -163,14 +209,17 @@ pub mod secp256k1 { false } } - fn calc_non_hardened_data(key_bytes: &[u8; 33]) -> [u8; 33] { - *key_bytes - } } impl IsPublicKey for secp256k1_ecdsa::PublicKey { type SecretKey = secp256k1_ecdsa::SecretKey; } + + impl CalcData for secp256k1_ecdsa::PublicKey { + fn calc_data(key_bytes: &[u8; 33], _segment: NonHardened) -> [u8; 33] { + *key_bytes + } + } } /// A seed is an arbitrary bytestring used to create the root of the tree. @@ -194,7 +243,12 @@ impl Seed { Slip10::from_seed(self) } - pub fn derive(&self, chain: &Chain) -> crate::Result> { + pub fn derive(&self, chain: I) -> Slip10 + where + K: hazmat::IsSecretKey + hazmat::CalcData<::Item>, + I: Iterator, + ::Item: Segment, + { self.to_master_key().derive(chain) } } @@ -206,8 +260,12 @@ impl From for Seed { } } +/// Public bytestring that uniquely distinguishes different extended keys for the same key. pub type ChainCode = [u8; 32]; +/// Extended secret or public key, ie. a key extended with a chain code. +/// +/// Extended keys must be handled with care. Security implications are explained in BIP32. #[derive(ZeroizeOnDrop)] pub struct Slip10 { key: core::marker::PhantomData, @@ -249,6 +307,7 @@ impl Slip10 { pub fn to_extended_public_key(&self) -> Slip10 where K::PublicKey: hazmat::IsPublicKey, + K: hazmat::ToPublic, { Slip10::from_extended_secret_key(self) } @@ -260,18 +319,24 @@ impl From<&Seed> for Slip10 { } } -impl Slip10 { +impl Slip10 +where + K: hazmat::IsPublicKey, + K::SecretKey: hazmat::ToPublic, +{ pub fn from_extended_secret_key(esk: &Slip10) -> Self { let mut k = Self::new(); - k.ext[..33].copy_from_slice(&::calc_non_hardened_data( - esk.key_bytes(), - )); + k.ext[..33].copy_from_slice(&::to_public(esk.key_bytes())); k.ext[33..].copy_from_slice(esk.chain_code()); k } } -impl From<&Slip10> for Slip10 { +impl From<&Slip10> for Slip10 +where + K: hazmat::IsPublicKey, + K::SecretKey: hazmat::ToPublic, +{ fn from(esk: &Slip10) -> Self { Self::from_extended_secret_key(esk) } @@ -320,34 +385,25 @@ impl Slip10 { } } - pub fn derive(&self, chain: &Chain) -> crate::Result { - if K::ALLOW_NON_HARDENED && (K::ALLOW_HARDENED || chain.all_non_hardened()) - || (K::ALLOW_HARDENED && chain.all_hardened()) - { - let mut key: Self = self.clone(); - for segment in &chain.0 { - key = key.derive_child_key(segment); - } - Ok(key) - } else { - Err(crate::Error::InvalidArgumentError { - alg: "SLIP10", - expected: "hardened key index", - }) + pub fn derive(&self, chain: I) -> Self + where + K: hazmat::IsSecretKey + hazmat::CalcData<::Item>, + I: Iterator, + ::Item: Segment, + { + let mut key: Self = self.clone(); + for segment in chain { + key = key.derive_child_key(segment); } + key } - pub fn child_key(&self, segment: &Segment) -> crate::Result { - if K::ALLOW_NON_HARDENED && (K::ALLOW_HARDENED || !segment.is_hardened()) - || (K::ALLOW_HARDENED && segment.is_hardened()) - { - Ok(self.derive_child_key(segment)) - } else { - Err(crate::Error::InvalidArgumentError { - alg: "SLIP10", - expected: "hardened key index", - }) - } + pub fn child_key(&self, segment: S) -> Self + where + S: Segment, + K: hazmat::CalcData, + { + self.derive_child_key(segment) } fn ext_mut(&mut self) -> &mut [u8; 64] { @@ -370,21 +426,22 @@ impl Slip10 { K::is_key_valid(self.key_bytes()) } - fn calc_data(&self, hardened: bool) -> [u8; 33] { - if hardened { - *self.key_bytes() - } else { - debug_assert!(K::ALLOW_NON_HARDENED); - K::calc_non_hardened_data(self.key_bytes()) - } + fn calc_data(&self, segment: S) -> [u8; 33] + where + S: Segment, + K: hazmat::CalcData, + { + K::calc_data(self.key_bytes(), segment) } - fn derive_child_key(&self, segment: &Segment) -> Self { - debug_assert!(K::ALLOW_NON_HARDENED || segment.is_hardened()); - + fn derive_child_key(&self, segment: S) -> Self + where + S: Segment, + K: hazmat::CalcData, + { let mut data = [0u8; 33 + 4]; - data[..33].copy_from_slice(&self.calc_data(segment.is_hardened())); - data[33..].copy_from_slice(&segment.bs()); // ser32(i) + data[..33].copy_from_slice(&self.calc_data(segment)); + data[33..].copy_from_slice(&segment.ser32()); let mut key = Self::new(); HMAC_SHA512(&data, self.chain_code(), key.ext_mut()); @@ -406,88 +463,109 @@ impl TryFrom<&[u8; 65]> for Slip10 { } } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct Segment(pub u32); - -impl Segment { - pub fn is_hardened(&self) -> bool { - self.0 & Self::HARDEN_MASK != 0 - } - - pub fn bs(&self) -> [u8; 4] { - self.0.to_be_bytes() // ser32(i) +/// Segment of a derivation chain. +pub trait Segment: Copy + Into { + fn is_hardened(self) -> bool; + fn ser32(self) -> [u8; 4] { + self.into().to_be_bytes() } - - pub const HARDEN_MASK: u32 = 1 << 31; + fn harden(self) -> Hardened; + fn unharden(self) -> NonHardened; } -impl From for Segment { - fn from(i: u32) -> Self { - Self(i) - } +/// Error indicating unexpected/invalid segment hardening. +/// Some keys only accept certain segments: either hardened or non-hardened. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SegmentHardeningError { + /// Input segment is hardened, expected non-hardened segment only. + Hardened, + /// Input segment is non-hardened, expected hardened segment only. + NonHardened, } -impl From for u32 { - fn from(s: Segment) -> Self { - s.0 +impl From for crate::Error { + fn from(inner: SegmentHardeningError) -> Self { + crate::Error::Slip10Error(inner) } } -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct Chain(Vec); +const HARDEN_MASK: u32 = 1 << 31; -impl Chain { - pub fn empty() -> Self { - Self(Vec::new()) +/// `u32` type can represent both hardened and non-hardened segments. +impl Segment for u32 { + fn is_hardened(self) -> bool { + self & HARDEN_MASK != 0 } - - pub fn from_segments<'a, I: IntoIterator>(is: I) -> Self { - Self(is.into_iter().cloned().collect()) + fn harden(self) -> Hardened { + Hardened(self | HARDEN_MASK) } - - pub fn from_u32>(is: I) -> Self { - Self(is.into_iter().map(Segment).collect()) + fn unharden(self) -> NonHardened { + NonHardened(self & !HARDEN_MASK) } +} - pub fn from_u32_hardened>(is: I) -> Self { - Self::from_u32(is.into_iter().map(|i| Segment::HARDEN_MASK | i)) - } +/// Type of hardened segments. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Hardened(u32); - pub fn join>(&self, o: O) -> Self { - let mut ss = self.0.clone(); - ss.extend_from_slice(&o.as_ref().0); - Self(ss) +impl From for u32 { + fn from(segment: Hardened) -> u32 { + segment.0 } +} - pub fn is_empty(&self) -> bool { - self.0.is_empty() +impl TryFrom for Hardened { + type Error = SegmentHardeningError; + fn try_from(segment: u32) -> Result { + if segment.is_hardened() { + Ok(Hardened(segment)) + } else { + Err(SegmentHardeningError::NonHardened) + } } +} - pub fn len(&self) -> usize { - self.0.len() +impl Segment for Hardened { + fn is_hardened(self) -> bool { + true } - - pub fn segments(&self) -> &[Segment] { - &self.0 + fn harden(self) -> Hardened { + self } - - pub fn all_hardened(&self) -> bool { - self.0.iter().all(Segment::is_hardened) + fn unharden(self) -> NonHardened { + NonHardened(self.0 ^ HARDEN_MASK) } +} - pub fn all_non_hardened(&self) -> bool { - !self.0.iter().any(Segment::is_hardened) +/// Type of non-hardened segments. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct NonHardened(u32); + +impl From for u32 { + fn from(segment: NonHardened) -> u32 { + segment.0 } } -impl Default for Chain { - fn default() -> Self { - Chain::empty() +impl TryFrom for NonHardened { + type Error = SegmentHardeningError; + fn try_from(segment: u32) -> Result { + if !segment.is_hardened() { + Ok(NonHardened(segment)) + } else { + Err(SegmentHardeningError::Hardened) + } } } -impl AsRef for Chain { - fn as_ref(&self) -> &Self { +impl Segment for NonHardened { + fn is_hardened(self) -> bool { + false + } + fn harden(self) -> Hardened { + Hardened(self.0 ^ HARDEN_MASK) + } + fn unharden(self) -> NonHardened { self } } diff --git a/tests/fixtures/slip10_ed25519.rs b/tests/fixtures/slip10_ed25519.rs index 711a564e..07c04c7d 100644 --- a/tests/fixtures/slip10_ed25519.rs +++ b/tests/fixtures/slip10_ed25519.rs @@ -6,32 +6,32 @@ master_private_key: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", chains: vec![ TestChain { - chain: Chain::empty(), + chain: vec![], chain_code: "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb", private_key: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", }, TestChain { - chain: Chain::from_u32_hardened(vec![0]), + chain: vec![0_u32].into_iter().map(|s| s.harden().into()).collect(), chain_code: "8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69", private_key: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 1]), + chain: vec![0_u32, 1].into_iter().map(|s| s.harden().into()).collect(), chain_code: "a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14", private_key: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 1, 2]), + chain: vec![0_u32, 1, 2].into_iter().map(|s| s.harden().into()).collect(), chain_code: "2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c", private_key: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 1, 2, 2]), + chain: vec![0_u32, 1, 2, 2].into_iter().map(|s| s.harden().into()).collect(), chain_code: "8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc", private_key: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 1, 2, 2, 1000000000]), + chain: vec![0_u32, 1, 2, 2, 1000000000].into_iter().map(|s| s.harden().into()).collect(), chain_code: "68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230", private_key: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", }, @@ -44,32 +44,32 @@ master_private_key: "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", chains: vec![ TestChain { - chain: Chain::empty(), + chain: vec![], chain_code: "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b", private_key: "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", }, TestChain { - chain: Chain::from_u32_hardened(vec![0]), + chain: vec![0_u32].into_iter().map(|s| s.harden().into()).collect(), chain_code: "0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d", private_key: "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 2147483647]), + chain: vec![0_u32, 2147483647].into_iter().map(|s| s.harden().into()).collect(), chain_code: "138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f", private_key: "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 2147483647, 1]), + chain: vec![0_u32, 2147483647, 1].into_iter().map(|s| s.harden().into()).collect(), chain_code: "73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90", private_key: "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 2147483647, 1, 2147483646]), + chain: vec![0_u32, 2147483647, 1, 2147483646].into_iter().map(|s| s.harden().into()).collect(), chain_code: "0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a", private_key: "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72", }, TestChain { - chain: Chain::from_u32_hardened(vec![0, 2147483647, 1, 2147483646, 2]), + chain: vec![0_u32, 2147483647, 1, 2147483646, 2].into_iter().map(|s| s.harden().into()).collect(), chain_code: "5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4", private_key: "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d", }, diff --git a/tests/fixtures/slip10_secp256k1.rs b/tests/fixtures/slip10_secp256k1.rs index 760da9a8..7ac7d8c3 100644 --- a/tests/fixtures/slip10_secp256k1.rs +++ b/tests/fixtures/slip10_secp256k1.rs @@ -6,32 +6,32 @@ master_private_key: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", chains: vec![ TestChain { - chain: Chain::empty(), + chain: vec![], chain_code: "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", private_key: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", }, TestChain { - chain: Chain::from_u32(vec![Segment::HARDEN_MASK | 0]), + chain: vec![0.harden().into()], chain_code: "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141", private_key: "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea", }, TestChain { - chain: Chain::from_u32(vec![Segment::HARDEN_MASK | 0, 1]), + chain: vec![0.harden().into(), 1], chain_code: "2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19", private_key: "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368", }, TestChain { - chain: Chain::from_u32(vec![Segment::HARDEN_MASK | 0, 1, Segment::HARDEN_MASK | 2]), + chain: vec![0.harden().into(), 1, 2.harden().into()], chain_code: "04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f", private_key: "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca", }, TestChain { - chain: Chain::from_u32(vec![Segment::HARDEN_MASK | 0, 1, Segment::HARDEN_MASK | 2, 2]), + chain: vec![0.harden().into(), 1, 2.harden().into(), 2], chain_code: "cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd", private_key: "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4", }, TestChain { - chain: Chain::from_u32(vec![Segment::HARDEN_MASK | 0, 1, Segment::HARDEN_MASK | 2, 2, 1000000000]), + chain: vec![0.harden().into(), 1, 2.harden().into(), 2, 1000000000], chain_code: "c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e", private_key: "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8", }, @@ -44,32 +44,32 @@ master_private_key: "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e", chains: vec![ TestChain { - chain: Chain::empty(), + chain: vec![], chain_code: "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689", private_key: "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e", }, TestChain { - chain: Chain::from_u32(vec![0]), + chain: vec![0], chain_code: "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c", private_key: "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e", }, TestChain { - chain: Chain::from_u32(vec![0, Segment::HARDEN_MASK | 2147483647]), + chain: vec![0, 2147483647.harden().into()], chain_code: "be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9", private_key: "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93", }, TestChain { - chain: Chain::from_u32(vec![0, Segment::HARDEN_MASK | 2147483647, 1]), + chain: vec![0, 2147483647.harden().into(), 1], chain_code: "f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb", private_key: "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7", }, TestChain { - chain: Chain::from_u32(vec![0, Segment::HARDEN_MASK | 2147483647, 1, Segment::HARDEN_MASK | 2147483646]), + chain: vec![0, 2147483647.harden().into(), 1, 2147483646.harden().into()], chain_code: "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29", private_key: "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d", }, TestChain { - chain: Chain::from_u32(vec![0, Segment::HARDEN_MASK | 2147483647, 1, Segment::HARDEN_MASK | 2147483646, 2]), + chain: vec![0, 2147483647.harden().into(), 1, 2147483646.harden().into(), 2], chain_code: "9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271", private_key: "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23", }, diff --git a/tests/slip10.rs b/tests/slip10.rs index 81a5dce8..8140063e 100644 --- a/tests/slip10.rs +++ b/tests/slip10.rs @@ -5,13 +5,10 @@ mod slip10 { #![allow(clippy::identity_op)] - use crypto::{ - keys::slip10::{Chain, Seed}, - Result, - }; + use crypto::{keys::slip10::Seed, Result}; struct TestChain { - chain: Chain, + chain: Vec, chain_code: &'static str, private_key: &'static str, } @@ -25,6 +22,7 @@ mod slip10 { #[cfg(feature = "ed25519")] fn run_ed25519_test_vectors(tvs: &[TestVector]) -> Result<()> { + use crypto::keys::slip10; use crypto::signatures::ed25519; for tv in tvs { @@ -40,7 +38,8 @@ mod slip10 { assert_eq!(expected_master_private_key, *m.secret_key().to_bytes()); for c in tv.chains.iter() { - let ck = seed.derive::(&c.chain)?; + let hardened_chain = c.chain.iter().cloned().map(|segment| segment.try_into().unwrap()); + let ck: slip10::Slip10 = seed.derive(hardened_chain); let mut expected_chain_code = [0u8; 32]; hex::decode_to_slice(c.chain_code, &mut expected_chain_code as &mut [u8]).unwrap(); @@ -57,7 +56,7 @@ mod slip10 { #[cfg(feature = "secp256k1")] fn run_secp256k1_test_vectors(tvs: &[TestVector]) -> Result<()> { - use crypto::keys::slip10::Segment; + use crypto::keys::slip10; use crypto::signatures::secp256k1_ecdsa; for tv in tvs { @@ -73,7 +72,7 @@ mod slip10 { assert_eq!(expected_master_private_key, *m.secret_key().to_bytes()); for c in tv.chains.iter() { - let ck = seed.derive::(&c.chain)?; + let ck = seed.derive::(c.chain.iter().cloned()); let mut expected_chain_code = [0u8; 32]; hex::decode_to_slice(c.chain_code, &mut expected_chain_code as &mut [u8]).unwrap(); @@ -83,19 +82,24 @@ mod slip10 { hex::decode_to_slice(c.private_key, &mut expected_private_key as &mut [u8]).unwrap(); assert_eq!(expected_private_key, *ck.secret_key().to_bytes()); - let last_segment_non_hardened = !c.chain.segments().last().map_or(true, Segment::is_hardened); + let last_segment_non_hardened = !c + .chain + .iter() + .cloned() + .last() + .map_or(true, slip10::Segment::is_hardened); if last_segment_non_hardened { let esk = seed.to_master_key::(); - let (head, tail) = c.chain.segments().split_at(c.chain.len() - 1); - let chain = Chain::from_segments(head); - let segment = tail[0]; - let esk = esk.derive(&chain).unwrap(); + let (head, tail) = c.chain.split_at(c.chain.len() - 1); + let chain = head; + let segment: slip10::NonHardened = tail[0].try_into().unwrap(); + let esk = esk.derive(chain.iter().cloned()); let epk = esk.to_extended_public_key(); assert_eq!(esk.chain_code(), epk.chain_code()); assert_eq!(esk.secret_key().public_key(), epk.public_key()); - let esk = esk.child_key(&segment).unwrap(); - let epk = epk.child_key(&segment).unwrap(); + let esk = esk.child_key(segment); + let epk = epk.child_key(segment); assert_eq!(expected_chain_code, *esk.chain_code()); assert_eq!(expected_private_key, *esk.secret_key().to_bytes()); assert_eq!(esk.chain_code(), epk.chain_code()); @@ -110,27 +114,11 @@ mod slip10 { #[cfg(feature = "ed25519")] #[test] fn ed25519_test_vectors() -> Result<()> { + use crypto::keys::slip10::Segment; let tvs = include!("fixtures/slip10_ed25519.rs"); run_ed25519_test_vectors(&tvs) } - #[cfg(feature = "ed25519")] - #[test] - fn ed25519_non_hardened() -> Result<()> { - use crypto::signatures::ed25519; - - let seed = Seed::from_bytes(&[1]); - let esk = seed.to_master_key::(); - // let epk = esk.to_extended_public_key(); - assert!(esk.derive(&Chain::from_u32([0, 1, 2])).is_err()); - assert!(esk.derive(&Chain::from_u32([0, 0x80000001, 2])).is_err()); - assert!(esk - .derive(&Chain::from_u32([0x80000000, 0x80000001, 0x80000002])) - .is_ok()); - - Ok(()) - } - #[cfg(feature = "secp256k1")] #[test] fn secp256k1_test_vectors() -> Result<()> { @@ -139,53 +127,56 @@ mod slip10 { run_secp256k1_test_vectors(&tvs) } - #[cfg(feature = "secp256k1")] - #[test] - fn secp256k1_public_key_test() { - use crypto::keys::slip10::{Segment, Slip10}; - use crypto::signatures::secp256k1_ecdsa; - - let seed = Seed::from_bytes(&[1]); - let esk = seed.to_master_key::(); - let epk = esk.to_extended_public_key(); - let mut epk_bytes = *epk.extended_bytes(); - assert_eq!(2, epk_bytes[0]); - epk_bytes[0] = 5; - assert!(Slip10::::try_from_extended_bytes(&epk_bytes).is_err()); - - assert!(esk.derive(&Chain::from_u32([0, 1, 2])).is_ok()); - assert!(esk.derive(&Chain::from_u32([0, 0x80000001, 2])).is_ok()); - assert!(esk - .derive(&Chain::from_u32([0x80000000, 0x80000001, 0x80000002])) - .is_ok()); - - assert!(esk.child_key(&Segment(0)).is_ok()); - assert!(esk.child_key(&Segment(0x80000000)).is_ok()); - - assert!(epk.derive(&Chain::from_u32([0, 1, 2])).is_ok()); - assert!(epk.derive(&Chain::from_u32([0, 0x80000001, 2])).is_err()); - assert!(epk - .derive(&Chain::from_u32([0x80000000, 0x80000001, 0x80000002])) - .is_err()); - - assert!(epk.child_key(&Segment(0)).is_ok()); - assert!(epk.child_key(&Segment(0x80000000)).is_err()); - } - #[cfg(feature = "secp256k1")] #[test] fn secp256k1_chain_test() { use crypto::signatures::secp256k1_ecdsa; let _ = Seed::from_bytes(&[1]) - .derive::(&Chain::from_u32([0, 1, 2])) - .unwrap() - .derive(&Chain::from_u32([0, 0x80000001, 2])) - .unwrap() - .derive(&Chain::from_u32([0x80000000, 0x80000001, 0x80000002])) - .unwrap() + .derive::([0, 1, 2].into_iter()) + .derive([0, 0x80000001, 2].into_iter()) + .derive([0x80000000, 0x80000001, 0x80000002].into_iter()) .secret_key() .public_key() .to_bytes(); } + + #[test] + #[allow(dead_code, unused_variables)] + fn test_generic() { + use crypto::keys::slip10; + + fn derive0>( + ext: &slip10::Slip10, + ) -> slip10::Slip10 { + use slip10::Segment; + ext.child_key(0.harden()) + } + + fn derive1>( + ext: &slip10::Slip10, + ) -> slip10::Slip10 { + use slip10::Segment; + ext.child_key(0.unharden()) + } + + let seed = slip10::Seed::from_bytes(&[0]); + + #[cfg(feature = "ed25519")] + { + use crypto::signatures::ed25519; + let ext_sk = slip10::Slip10::::from_seed(&seed); + let _ = derive0(&ext_sk); + } + + #[cfg(feature = "secp256k1")] + { + use crypto::signatures::secp256k1_ecdsa; + let ext_sk = slip10::Slip10::::from_seed(&seed); + let ext_sk = derive0(&ext_sk); + let _ = derive1(&ext_sk); + let ext_pk = ext_sk.to_extended_public_key(); + let _ = derive1(&ext_pk); + } + } }