From 9bd5655db3258419a90928b1d156e3d2e4044dc1 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 25 Nov 2024 12:37:27 +0100 Subject: [PATCH] Add stark test vectors Signed-off-by: Denis Varlakov --- Cargo.toml | 20 ++- examples/generate_test_vectors.rs | 103 +++++++++++++++ src/edwards.rs | 2 - src/lib.rs | 4 + src/slip10.rs | 8 +- src/stark.rs | 132 +++++++++++++++++++ tests/edwards_test_vector.rs | 2 + tests/stark_test_vector.rs | 205 ++++++++++++++++++++++++++++++ 8 files changed, 466 insertions(+), 10 deletions(-) create mode 100644 examples/generate_test_vectors.rs create mode 100644 src/stark.rs create mode 100644 tests/stark_test_vector.rs diff --git a/Cargo.toml b/Cargo.toml index a29376e..f4c7217 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,28 +35,38 @@ curve-secp256k1 = ["generic-ec/curve-secp256k1", "slip10"] curve-secp256r1 = ["generic-ec/curve-secp256r1", "slip10"] # Enables Edwards-specific derivation curve-ed25519 = ["generic-ec/curve-ed25519", "edwards"] +# Enables Stark-specific derivation +curve-stark = ["generic-ec/curve-stark", "stark"] all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-ed25519"] serde = ["dep:serde", "generic-ec/serde"] # Enables Slip10 derivation -slip10 = ["hmac", "sha2", "generic-array", "subtle"] +slip10 = ["subtle", "hmac", "sha2", "generic-array"] # Enables Edwards-specific derivation -edwards = ["hmac", "sha2", "generic-array", "curve-ed25519"] +edwards = ["curve-ed25519", "hmac", "sha2", "generic-array"] # Enables Stark-specific derivation -stark = [] +stark = ["curve-stark", "hmac", "sha2", "generic-array"] [[test]] name = "slip10_test_vector" -required-features = ["curve-secp256k1", "curve-secp256r1"] +required-features = ["slip10", "curve-secp256k1", "curve-secp256r1"] [[test]] name = "edwards_test_vector" -required-features = ["curve-ed25519"] +required-features = ["edwards"] + +[[test]] +name = "stark_test_vector" +required-features = ["stark"] [[example]] name = "curves_analysis" required-features = ["all-curves"] +[[example]] +name = "generate_test_vectors" +required-features = ["all-curves"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "katex-header.html"] diff --git a/examples/generate_test_vectors.rs b/examples/generate_test_vectors.rs new file mode 100644 index 0000000..eb0d923 --- /dev/null +++ b/examples/generate_test_vectors.rs @@ -0,0 +1,103 @@ +use generic_ec::{Curve, Point, SecretScalar}; +use rand::Rng; + +fn main() { + generate_test_vectors::(); +} + +fn generate_test_vectors>() { + let mut rng = rand::thread_rng(); + + let sk = SecretScalar::::random(&mut rng); + let pk = Point::generator() * &sk; + let chain_code: hd_wallet::ChainCode = rng.gen(); + + println!("sk: {}", hex::encode(sk.as_ref().to_be_bytes())); + println!("pk: {}", hex::encode(pk.to_bytes(true))); + println!("chain code: {}", hex::encode(chain_code)); + + let paths: &[&[u32]] = &[ + // Non-hardened derivation + &[0], + &[1], + &[2], + &[ + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + ], + &[ + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + ], + // Hardened derivation + &[0 + hd_wallet::H], + &[1 + hd_wallet::H], + &[2 + hd_wallet::H], + // Mixed hardened and non-hardened derivation + &[ + rng.gen_range(0..hd_wallet::H), + rng.gen_range(hd_wallet::H..=u32::MAX), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(hd_wallet::H..=u32::MAX), + ], + &[ + rng.gen_range(0..hd_wallet::H), + rng.gen_range(0..hd_wallet::H), + rng.gen_range(hd_wallet::H..=u32::MAX), + rng.gen_range(hd_wallet::H..=u32::MAX), + ], + ]; + + let key = hd_wallet::ExtendedSecretKey { + secret_key: sk, + chain_code, + }; + let key = hd_wallet::ExtendedKeyPair::from(key); + assert_eq!(key.public_key().public_key, pk); + + for path in paths { + println!("{:?}", PathFmt(path)); + let child_key = Hd::derive_child_key_pair_with_path(&key, path.iter().copied()); + println!( + "child secret key: {}", + hex::encode(child_key.secret_key().secret_key.as_ref().to_be_bytes()) + ); + println!( + "child public key: {}", + hex::encode(child_key.public_key().public_key.to_bytes(true)) + ); + println!("child chain code: {}", hex::encode(child_key.chain_code())); + println!(); + } +} + +struct PathFmt<'a>(&'a [u32]); + +impl<'a> std::fmt::Debug for PathFmt<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[")?; + for (is_first, index) in std::iter::once(true) + .chain(std::iter::repeat(false)) + .zip(self.0) + { + if !is_first { + write!(f, ", ")?; + } + + if *index < hd_wallet::H { + write!(f, "{index}")?; + } else { + write!( + f, + "{} + hd_wallet::H", + index.checked_sub(hd_wallet::H).unwrap() + )?; + } + } + write!(f, "]") + } +} diff --git a/src/edwards.rs b/src/edwards.rs index f040122..2770609 100644 --- a/src/edwards.rs +++ b/src/edwards.rs @@ -49,7 +49,6 @@ impl DeriveShift for Edwards { let hmac = HmacSha512::new_from_slice(&parent_public_key.chain_code) .expect("this never fails: hmac can handle keys of any size"); let i = hmac - .clone() .chain_update(parent_public_key.public_key.to_bytes(true)) // we append 0 byte to the public key for compatibility with other libs .chain_update([0x00]) @@ -66,7 +65,6 @@ impl DeriveShift for Edwards { let hmac = HmacSha512::new_from_slice(parent_key.chain_code()) .expect("this never fails: hmac can handle keys of any size"); let i = hmac - .clone() .chain_update([0x00]) .chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes()) .chain_update(child_index.to_be_bytes()) diff --git a/src/lib.rs b/src/lib.rs index 91bb8ec..4178091 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,11 +80,15 @@ pub mod edwards; pub mod errors; #[cfg(feature = "slip10")] pub mod slip10; +#[cfg(feature = "stark")] +pub mod stark; #[cfg(feature = "edwards")] pub use edwards::Edwards; #[cfg(feature = "slip10")] pub use slip10::Slip10; +#[cfg(feature = "stark")] +pub use stark::Stark; /// Beginning of hardened child indexes /// diff --git a/src/slip10.rs b/src/slip10.rs index a8acb3e..ec28a49 100644 --- a/src/slip10.rs +++ b/src/slip10.rs @@ -33,11 +33,13 @@ type HmacSha512 = hmac::Hmac; /// secret keys are represented as scalars and public keys as points, see [`ExtendedSecretKey`] /// and [`ExtendedPublicKey`]. /// -/// If you need HD derivation on Ed25519 curve, we recommend using [`Edwards`] HD derivation, -/// which supports both hardened and non-hardened derivation. +/// [`ExtendedSecretKey`]: crate::ExtendedSecretKey +/// +/// If you need HD derivation on Ed25519 curve, we recommend using [`Edwards`](crate::Edwards) HD +/// derivation, which supports both hardened and non-hardened derivation. /// /// ## Master key derivation from the seed -/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined +/// [`derive_master_key`] can be used to derive a master key from the seed as defined /// in the spec. /// /// ## Example diff --git a/src/stark.rs b/src/stark.rs new file mode 100644 index 0000000..e3d5716 --- /dev/null +++ b/src/stark.rs @@ -0,0 +1,132 @@ +//! Stark HD derivation +//! +//! This module provides [`Stark`] derivation as well as aliases for calling +//! `>::*` methods for convenience when you don't need to support +//! generic HD derivation algorithm. +//! +//! See [`Stark`] docs to learn more about the derivation method. + +use generic_ec::{curves, Point, Scalar}; +use hmac::Mac; + +use crate::{ + DeriveShift, DerivedShift, ExtendedKeyPair, ExtendedPublicKey, HardenedIndex, NonHardenedIndex, +}; + +type HmacSha512 = hmac::Hmac; + +/// HD derivation for stark curve +/// +/// ## Algorithm +/// The algorithm is a modification of BIP32: +/// +/// ```text +/// def derive_child_key(parent_public_key[, parent_secret_key], parent_chain_code, child_index): +/// if is_hardened(child_index): +/// i = HMAC_SHA512(key = parent_chain_code, 0x00 || 0x00 || parent_secret_key || child_index) +/// || HMAC_SHA512(key = parent_chain_code, 0x01 || 0x00 || parent_secret_key || child_index) +/// else: +/// i = HMAC_SHA512(key = parent_chain_code, 0x00 || parent_public_key || child_index) +/// || HMAC_SHA512(key = parent_chain_code, 0x01 || parent_public_key || child_index) +/// shift = i[..96] mod order +/// child_secret_key = parent_secret_key + shift and/or child_public_key = parent_public_key + shift G +/// child_chain_code = i[N..] +/// return child_public_key[, child_secret_key], child_chain_code +/// ``` +/// +/// ## Other known methods for stark HD derivation +/// There's another known method for HD derivation on stark curve implemented in +/// [argent-x], which basically derives secp256k1 child key from a seed, and then +/// uses grinding function to deterministically convert it into stark key. +/// +/// We decided not to implement it due to its cons: +/// * No support for non-hardened derivation +/// * Grinding is a probabilistic algorithm which does a lot of hashing (32 hashes +/// on average, but in worst case can be 1000+). +/// * In general, it's strange to derive secp256k1 key and then convert it to stark key +/// +/// Our derivation algorithm addresses these flaws: it yields a stark key right away (without +/// any intermediate secp256k1 keys), supports non-hardened derivation, does only 2 hashes per +/// derivation. +/// +/// [argent-x]: https://github.com/argentlabs/argent-x/blob/13142607d83fea10b297d6a23452e810605784d1/packages/extension/src/shared/signer/ArgentSigner.ts#L14-L25, +pub struct Stark; + +impl DeriveShift for Stark { + fn derive_public_shift( + parent_public_key: &ExtendedPublicKey, + child_index: NonHardenedIndex, + ) -> DerivedShift { + let hmac = HmacSha512::new_from_slice(&parent_public_key.chain_code) + .expect("this never fails: hmac can handle keys of any size"); + let i0 = hmac + .clone() + .chain_update([0x00]) + .chain_update(parent_public_key.public_key.to_bytes(true)) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + let i1 = hmac + .chain_update([0x01]) + .chain_update(parent_public_key.public_key.to_bytes(true)) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(parent_public_key, i0, i1) + } + + fn derive_hardened_shift( + parent_key: &ExtendedKeyPair, + child_index: HardenedIndex, + ) -> DerivedShift { + let hmac = HmacSha512::new_from_slice(parent_key.chain_code()) + .expect("this never fails: hmac can handle keys of any size"); + let i0 = hmac + .clone() + .chain_update([0x00]) + .chain_update([0x00]) + .chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes()) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + let i1 = hmac + .chain_update([0x01]) + .chain_update([0x00]) + .chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes()) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(&parent_key.public_key, i0, i1) + } +} + +impl Stark { + fn calculate_shift( + parent_public_key: &ExtendedPublicKey, + i0: hmac::digest::Output, + i1: hmac::digest::Output, + ) -> DerivedShift { + let i = generic_array::sequence::Concat::concat(i0, i1); + let (shift, chain_code) = split(&i); + + let shift = Scalar::from_be_bytes_mod_order(shift); + let child_pk = parent_public_key.public_key + Point::generator() * shift; + + DerivedShift { + shift, + child_public_key: ExtendedPublicKey { + public_key: child_pk, + chain_code: (*chain_code).into(), + }, + } + } +} + +fn split( + i: &generic_array::GenericArray, +) -> ( + &generic_array::GenericArray, + &generic_array::GenericArray, +) { + generic_array::sequence::Split::split(i) +} diff --git a/tests/edwards_test_vector.rs b/tests/edwards_test_vector.rs index 5a9af77..08ffdf6 100644 --- a/tests/edwards_test_vector.rs +++ b/tests/edwards_test_vector.rs @@ -15,6 +15,8 @@ struct Derivation { expected_public_key: [u8; 32], } +/// These test vectors were obtained by running HD derivation in another library +/// that implements edwards HD derivation const TEST_VECTORS: &[TestVector] = &[TestVector { root_secret_key: hex!("09ba1ad29fabe87a0cf23fec142db2adfb8f9e7089928000dcba5714e08236ec"), root_public_key: hex!("6fa093b0e855f5fdb40d77f6efe9b67b709092a71d73f35de6afc70cac40d57a"), diff --git a/tests/stark_test_vector.rs b/tests/stark_test_vector.rs new file mode 100644 index 0000000..088cd50 --- /dev/null +++ b/tests/stark_test_vector.rs @@ -0,0 +1,205 @@ +use hd_wallet::HdWallet; +use hex_literal::hex; + +struct TestVector { + root_secret_key: [u8; 32], + root_public_key: [u8; 33], + chain_code: hd_wallet::ChainCode, + derivations: &'static [Derivation], +} + +struct Derivation { + path: &'static [u32], + + expected_secret_key: [u8; 32], + expected_public_key: [u8; 33], + expected_chain_code: [u8; 32], +} + +/// These test vectors were obtained by running: +/// +/// ```bash +/// cargo run --all-features --example generate_test_vectors +/// ``` +const TEST_VECTORS: &[TestVector] = &[TestVector { + root_secret_key: hex!("0488a73eebe871ec429b37cdbd1dc160d4d2faa34556c8e9a76e8cd51b6cfbb8"), + root_public_key: hex!("0205c89b4a3d5f4650cd63a48baf0df21280ce0fb85ea872853de3ce18e1e61223"), + chain_code: hex!("9e88c71e0435e16662469e464ba3bd242b4c99f6ff54960cb682edceb42223ed"), + derivations: &[ + // Non-hardened derivation + Derivation { + path: &[0], + expected_secret_key: hex!( + "0597872991a7759365513efb163a8c28b14f10b969f11da6fd3653f92e2d57ca" + ), + expected_public_key: hex!( + "030625a1ed01a061183d9b8bfc87ea6fa3a788195250516415ed6e196a070de55f" + ), + expected_chain_code: hex!( + "fed5a2ca5e8321c7f95c46539047eb50faf13e026e3d41d075540c1a9cf1d380" + ), + }, + Derivation { + path: &[1], + expected_secret_key: hex!( + "07e57c758044ba70ea08e0edb43ea1eb1a38d118d431560bcbcd6785cc999b70" + ), + expected_public_key: hex!( + "0203cdcfb32317c7b7d5ada6218145e89bae0190b97e1c0b3b982018a59b279975" + ), + expected_chain_code: hex!( + "b615bd8245089297147a77915bda2ffc0a142e1d333844b31a964e601ff89987" + ), + }, + Derivation { + path: &[2], + expected_secret_key: hex!( + "06b2ba84b7b0c1df4e3c8c579a6f53e7c8181bc816008050499c80c2426009c8" + ), + expected_public_key: hex!( + "02072fafedc815e5817cb3203b328074c7a9deca71f93f3a9729a6910323c4f5aa" + ), + expected_chain_code: hex!( + "5ca62c03e8c03bbb2717fedda726d28ce76982ac45ae69110ab48320470fc17d" + ), + }, + Derivation { + path: &[1951614227, 785687956, 1701190516, 997951189], + expected_secret_key: hex!( + "07eddf7b3dec89605d3d15392d9ed3e985407775a58dbaea51ad55e415b45494" + ), + expected_public_key: hex!( + "030430e999917068df34f32c35beff2a1caf0d2cd4ed0defe666249d63e031379b" + ), + expected_chain_code: hex!( + "c1fac37faab34cdbf2d8cfc39a16dce21b615604cd07e4586729ff3f03582594" + ), + }, + Derivation { + path: &[2122015632, 1105344888, 1598870552, 200678536], + expected_secret_key: hex!( + "0691c9420bf4f47011cb0448993b8f141a15c072ffe22cb417e05979327b426c" + ), + expected_public_key: hex!( + "0305343fd6ac0ed00978277e62366c029b151044119b339fec8e61254168318a7b" + ), + expected_chain_code: hex!( + "b25e59c29f1894a5d716c2e72d251f34ee8be02194e474329607f7c707a37c59" + ), + }, + // Hardened derivation + Derivation { + path: &[0 + hd_wallet::H], + expected_secret_key: hex!( + "0075fc3585d3bedd04c7cd0d7fe871f3e52d5ab9c4bfcc6cc626957cbe45545c" + ), + expected_public_key: hex!( + "030789614405216d8cc36454c4108975c0d2d66035ecd6a2bd8c3dca628b3ab546" + ), + expected_chain_code: hex!( + "ebe602cef4168ba94fb3b43b442ad67bd91f62039896887a140baa1c4585c470" + ), + }, + Derivation { + path: &[1 + hd_wallet::H], + expected_secret_key: hex!( + "03e9223be5643943972f68584119c2b349ea4dedf9be0a52943c125fed0a1998" + ), + expected_public_key: hex!( + "03017457b4d6f24a0a9e449e1b2dba89afef7fd985d5500569285365913d454829" + ), + expected_chain_code: hex!( + "c71dde186014797faf2d54629600c31e5277c54c7274136c61a46a6a358dec34" + ), + }, + Derivation { + path: &[2 + hd_wallet::H], + expected_secret_key: hex!( + "0005ed54ac0e6f1570447183c678e33eb370b7d5d6772ba1fb27b856f0bf74dd" + ), + expected_public_key: hex!( + "02066f79bdeae47d079560e09b9ecde60ec4ac536fa6ac5794d7c850300cabdbce" + ), + expected_chain_code: hex!( + "26b7f3e97f5ab4db4c3308799e7cc6e45be74787a1eafd6d14dcc34a8a598cca" + ), + }, + // Mixed hardened and non-hardened derivation + Derivation { + path: &[ + 251411281, + 923229946 + hd_wallet::H, + 496028387, + 1163470860 + hd_wallet::H, + ], + expected_secret_key: hex!( + "01bf3dce889f48ce16cd2df102b32b2362fb805a9bf33474d391891bd650cbf6" + ), + expected_public_key: hex!( + "02038b86fc2cf121a2216660d7fe8a2f68c28b80f645641a8e9554bbc2bae2a56e" + ), + expected_chain_code: hex!( + "021b195e90deec762d078f51d729720f5d5ed3b9d44a98725b9abc90a596e4ff" + ), + }, + Derivation { + path: &[ + 182835681, + 2001004627, + 826457658 + hd_wallet::H, + 1973700623 + hd_wallet::H, + ], + expected_secret_key: hex!( + "07ac0e9e7b6792efdacb187b7bd44fbc9841f79ca7fbe8f0f016dc6f3ac8dbeb" + ), + expected_public_key: hex!( + "0307637c9e9df9184aa631dff4be452cc47b5f03e8dae8b6a78f9e4f8e7c0d8471" + ), + expected_chain_code: hex!( + "cc63852ec5ce6ac12af2b068c7f54fc5011356c717f9d7f43ab1471d412a85af" + ), + }, + ], +}]; + +#[test] +fn test_vectors() { + for vector in TEST_VECTORS { + let mut root_sk = + generic_ec::Scalar::::from_be_bytes(&vector.root_secret_key) + .expect("invalid root_sk"); + let root_sk = generic_ec::SecretScalar::new(&mut root_sk); + + let esk = hd_wallet::ExtendedSecretKey { + secret_key: root_sk, + chain_code: vector.chain_code, + }; + let ekey = hd_wallet::ExtendedKeyPair::from(esk); + + assert_eq!( + hex::encode(ekey.public_key().public_key.to_bytes(true)), + hex::encode(vector.root_public_key) + ); + + for derivation in vector.derivations { + eprintln!("path: {:?}", derivation.path); + let child_key = hd_wallet::Stark::derive_child_key_pair_with_path( + &ekey, + derivation.path.iter().copied(), + ); + + assert_eq!( + hex::encode(child_key.secret_key().secret_key.as_ref().to_be_bytes()), + hex::encode(derivation.expected_secret_key) + ); + assert_eq!( + hex::encode(child_key.public_key().public_key.to_bytes(true)), + hex::encode(derivation.expected_public_key) + ); + assert_eq!( + hex::encode(child_key.chain_code()), + hex::encode(derivation.expected_chain_code) + ); + } + } +}