From 898534d86f26ffdeccb1c9769f21f687f2d6e3e5 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 19 Nov 2024 10:52:00 +0100 Subject: [PATCH 1/8] Add curves analysis Signed-off-by: Denis Varlakov --- Cargo.lock | 172 +++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- examples/curves_analysis.rs | 98 ++++++++++++++++++++ 3 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 examples/curves_analysis.rs diff --git a/Cargo.lock b/Cargo.lock index 5cb99a9..349cae9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,29 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addchain" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2e69442aa5628ea6951fa33e24efe8313f4321a91bd729fc2f75bdfc858570" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "base16ct" version = "0.2.0" @@ -192,7 +209,7 @@ version = "3.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -205,10 +222,28 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "byteorder", + "ff_derive", "rand_core", "subtle", ] +[[package]] +name = "ff_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f54704be45ed286151c5e11531316eaef5b8f5af7d597b806fdb8af108d84a" +dependencies = [ + "addchain", + "cfg-if", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -277,6 +312,7 @@ dependencies = [ "p256", "rand_core", "sha2", + "stark-curve", "subtle", "zeroize", ] @@ -292,6 +328,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0205cd82059bc63b63cf516d714352a30c44f2c74da9961dfda2617ae6b5918" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "group" version = "0.13.0" @@ -310,9 +356,10 @@ dependencies = [ "generic-array", "generic-ec", "hex", - "hex-literal", + "hex-literal 0.4.1", "hmac", "rand", + "rug", "serde", "sha2", "subtle", @@ -324,6 +371,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + [[package]] name = "hex-literal" version = "0.4.1" @@ -361,6 +414,23 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -474,6 +544,18 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rug" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ae2c1089ec0575193eb9222881310cc1ed8bce3646ef8b81b44b518595b79d" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -555,6 +637,19 @@ dependencies = [ "digest", ] +[[package]] +name = "stark-curve" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b23613eb5053481a8b1ac807460fb34cff15d4301fd21b305cbd267268f177b" +dependencies = [ + "ff", + "hex-literal 0.3.4", + "primeorder", + "subtle", + "zeroize", +] + [[package]] name = "strsim" version = "0.11.1" @@ -613,6 +708,79 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index f62c698..0c75bf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,13 +22,16 @@ serde = { version = "1", default-features = false, features = ["derive"], option hex = "0.4" hex-literal = "0.4" rand = "0.8" +rug = "1.26" + +generic-ec = { version = "0.4", default-features = false, features = ["curve-stark"] } [features] std = [] curve-secp256k1 = ["generic-ec/curve-secp256k1"] curve-secp256r1 = ["generic-ec/curve-secp256r1"] curve-ed25519 = ["generic-ec/curve-ed25519"] -all-curves = ["curve-secp256k1", "curve-secp256r1"] +all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-ed25519"] serde = ["dep:serde", "generic-ec/serde"] [[test]] diff --git a/examples/curves_analysis.rs b/examples/curves_analysis.rs new file mode 100644 index 0000000..289ad48 --- /dev/null +++ b/examples/curves_analysis.rs @@ -0,0 +1,98 @@ +use generic_ec::Curve; +use rug::Complete; + +fn main() { + analyze_curve::(); + analyze_curve::(); + analyze_curve::(); + analyze_curve::(); +} + +fn analyze_curve() { + println!(); + println!("Curve: {}", E::CURVE_NAME); + + let order: rug::Integer = { + let order_minus_one = -generic_ec::Scalar::::one(); + let bytes_be = order_minus_one.to_be_bytes(); + let order_minus_one = rug::Integer::from_digits::(&bytes_be, rug::integer::Order::Msf); + order_minus_one + 1 + }; + + { + let prob = rug::Rational::from(( + rug::Integer::from(rug::Integer::ONE << 256) - &order, + rug::Integer::from(rug::Integer::ONE << 256), + )); + println!( + "Probability that random 32 bytes are invalid scalar: {prob_nom} in 2^256: {prob_f64:.4} in range {prob_range}", + prob_nom = prob.numer(), + prob_f64 = prob.to_f64(), + prob_range = find_closest_power_of_minus_two_str(&prob), + ); + } + + let (n, w) = rug::Integer::from(rug::Integer::ONE << 256).div_rem(order.clone()); + let n = n.to_u32().unwrap(); + + println!("order = {order}"); + println!("w = {w}"); + + println!("When we take rand(32 bytes) modulo curve_order, we have distribution:"); + { + let prob_actual = rug::Rational::from((n + 1, rug::Integer::ONE << 256)); + let prob_uniform = &rug::Rational::from((rug::Integer::ONE, &order)); + println!( + "- for any y < w, Pr[x = y] = {} in 2^256, diff with uniform in range: {range}", + n + 1, + range = find_closest_power_of_minus_two_str(&(prob_actual - prob_uniform).abs()) + ); + } + { + let prob_actual = rug::Rational::from((n + 1, rug::Integer::ONE << 256)); + let prob_uniform = &rug::Rational::from((rug::Integer::ONE, &order)); + println!( + "- for any y >= w, Pr[x = y] = {n} in 2^256, diff with uniform in range: {range}", + range = find_closest_power_of_minus_two_str(&(prob_actual - prob_uniform).abs()) + ); + } + + let stat_diff = { + let before_w_per_each = rug::Rational::from((n + 1, rug::Integer::ONE << 256)) + - rug::Rational::from((rug::Integer::ONE, &order)); + let before_w = (&w * before_w_per_each).abs(); + + let after_w_per_each = rug::Rational::from((n, rug::Integer::ONE << 256)) + - rug::Rational::from((rug::Integer::ONE, &order)); + let after_w = ((&order - &w).complete() * after_w_per_each).abs(); + + (before_w + after_w) / 2 + }; + + println!("statistical difference of actual and uniform distribution = {stat_diff}"); + println!( + "statistical difference lies in range: {}", + find_closest_power_of_minus_two_str(&stat_diff) + ); +} + +/// Given 0 < x < 1, returns `lo`, `hi` such that 2^-lo <= x <= 2^-hi +fn find_closest_power_of_minus_two(x: &rug::Rational) -> (u32, u32) { + assert!(x.is_positive() && x < rug::Rational::ONE); + + for (lo, hi) in (1..).zip(0..) { + let x_lo = rug::Rational::from((rug::Integer::ONE, rug::Integer::ONE << lo)); + let x_hi = rug::Rational::from((rug::Integer::ONE, rug::Integer::ONE << hi)); + if x_lo <= *x && *x <= x_hi { + return (lo, hi); + } + } + + unreachable!() +} + +/// Same as [`find_closest_power_of_minus_two`] but returns a formatted repr of the range +fn find_closest_power_of_minus_two_str(x: &rug::Rational) -> String { + let (lo, hi) = find_closest_power_of_minus_two(x); + format!("2^-{lo}..2^-{hi}") +} From 08cdae4084556b2869b3a3281ecad3686d303d14 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 21 Nov 2024 13:01:18 +0100 Subject: [PATCH 2/8] Improve curves analysis tool Signed-off-by: Denis Varlakov --- Cargo.toml | 4 ++ examples/curves_analysis.rs | 139 ++++++++++++++++++++++++------------ 2 files changed, 99 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c75bf4..7b437b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,10 @@ required-features = ["curve-secp256k1", "curve-secp256r1"] name = "edwards_test_vector" required-features = ["curve-ed25519"] +[[example]] +name = "curves_analysis" +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/curves_analysis.rs b/examples/curves_analysis.rs index 289ad48..87d82ab 100644 --- a/examples/curves_analysis.rs +++ b/examples/curves_analysis.rs @@ -1,14 +1,53 @@ use generic_ec::Curve; -use rug::Complete; +use rug::{ops::DivRounding, Complete}; fn main() { - analyze_curve::(); - analyze_curve::(); - analyze_curve::(); - analyze_curve::(); + println!( + "========================\n\n\ + This tool analyzes curves parameters, which should help determining the best HD \ + derivation algorithm to use. It provides following characteristics per each curve:\n\ + - Probability that rand(32 bytes) correspond to an invalid scalar on that curve\n\ + - Statistical difference of rand(N bytes) mod curve_order distribution with uniform \ + distribution\n\ + \n\ + After observing the parameters, skilled conscious observer makes a decision which \ + HD derivation algorithm works the best for that curve, but the general guidelines are:\n\ + 1. If probability that random 32 bytes do not correspond to a valid scalar is greater than \ + 2^-32, DO NOT use slip10-like derivation or any other algorithm based on rejection sampling\n\ + 2. DO NOT use `rand(N bytes) mod curve_order` if statistical difference of its distribution with \ + uniform is greater than 2^-80\n\ + \n========================" + ); + + analyze_curve::([32]); + analyze_curve::([32]); + analyze_curve::(32..=48); + analyze_curve::([32]); + + assert_eq!( + 48, + recommended_random_string_size::() + ) +} + +/// How many random bytes we need to have distribution indistinguishable from uniform when we take +/// the bytes mod curve order +/// +/// This function is taken from hash to curve RFC9380: https://datatracker.ietf.org/doc/rfc9380/ (see +/// section 5) +fn recommended_random_string_size() -> usize { + let order: rug::Integer = { + let order_minus_one = -generic_ec::Scalar::::one(); + let bytes_be = order_minus_one.to_be_bytes(); + let order_minus_one = rug::Integer::from_digits::(&bytes_be, rug::integer::Order::Msf); + order_minus_one + 1 + }; + let order_log2 = order.significant_bits(); + let l: rug::Integer = rug::Rational::from((order_log2 + 128, 8)).ceil_ref().into(); + l.try_into().unwrap() } -fn analyze_curve() { +fn analyze_curve(rand_bytes: impl IntoIterator) { println!(); println!("Curve: {}", E::CURVE_NAME); @@ -25,55 +64,58 @@ fn analyze_curve() { rug::Integer::from(rug::Integer::ONE << 256), )); println!( - "Probability that random 32 bytes are invalid scalar: {prob_nom} in 2^256: {prob_f64:.4} in range {prob_range}", - prob_nom = prob.numer(), + "Probability that random 32 bytes are invalid scalar: {prob_num} in 2^256: {prob_f64:.4} in range {prob_range}", + prob_num = prob.numer(), prob_f64 = prob.to_f64(), prob_range = find_closest_power_of_minus_two_str(&prob), ); } - let (n, w) = rug::Integer::from(rug::Integer::ONE << 256).div_rem(order.clone()); - let n = n.to_u32().unwrap(); - println!("order = {order}"); - println!("w = {w}"); - println!("When we take rand(32 bytes) modulo curve_order, we have distribution:"); - { - let prob_actual = rug::Rational::from((n + 1, rug::Integer::ONE << 256)); - let prob_uniform = &rug::Rational::from((rug::Integer::ONE, &order)); - println!( - "- for any y < w, Pr[x = y] = {} in 2^256, diff with uniform in range: {range}", - n + 1, - range = find_closest_power_of_minus_two_str(&(prob_actual - prob_uniform).abs()) - ); - } - { - let prob_actual = rug::Rational::from((n + 1, rug::Integer::ONE << 256)); - let prob_uniform = &rug::Rational::from((rug::Integer::ONE, &order)); - println!( - "- for any y >= w, Pr[x = y] = {n} in 2^256, diff with uniform in range: {range}", - range = find_closest_power_of_minus_two_str(&(prob_actual - prob_uniform).abs()) - ); - } + for rand_bytes in rand_bytes { + let rand_bits = rand_bytes * 8; + let rand_mod: rug::Integer = (rug::Integer::ONE << rand_bits).into(); - let stat_diff = { - let before_w_per_each = rug::Rational::from((n + 1, rug::Integer::ONE << 256)) - - rug::Rational::from((rug::Integer::ONE, &order)); - let before_w = (&w * before_w_per_each).abs(); + let (n, w) = rand_mod.div_rem_ref(&order).complete(); - let after_w_per_each = rug::Rational::from((n, rug::Integer::ONE << 256)) - - rug::Rational::from((rug::Integer::ONE, &order)); - let after_w = ((&order - &w).complete() * after_w_per_each).abs(); + let prob_uniform = rug::Rational::from((rug::Integer::ONE, &order)); + println!("When we take rand({rand_bytes} bytes) modulo curve_order, we have distribution:"); + { + let prob_actual = rug::Rational::from((&n + 1, &rand_mod)); + println!( + "- for any y < w, Pr[x = y] = {} in 2^{rand_bits}, diff with uniform in range: {range}", + rug::Integer::from(&n + 1), + range = find_closest_power_of_minus_two_str(&(prob_actual - &prob_uniform).abs()) + ); + } + { + let prob_actual = rug::Rational::from((&n, &rand_mod)); + println!( + "- for any y >= w, Pr[x = y] = {n} in 2^{rand_bits}, diff with uniform in range: {range}", + range = find_closest_power_of_minus_two_str(&(prob_actual - &prob_uniform).abs()) + ); + } + println!("where w = {w}"); - (before_w + after_w) / 2 - }; + let stat_diff = { + let before_w_per_each = + (rug::Rational::from((&n + 1, &rand_mod)) - &prob_uniform).abs(); + let before_w = &w * before_w_per_each; - println!("statistical difference of actual and uniform distribution = {stat_diff}"); - println!( - "statistical difference lies in range: {}", - find_closest_power_of_minus_two_str(&stat_diff) - ); + let after_w_per_each = (rug::Rational::from((&n, &rand_mod)) - &prob_uniform).abs(); + let after_w = (&order - &w).complete() * after_w_per_each; + + (before_w + after_w) / 2 + }; + + println!( + "statistical difference between actual and uniform distribution is in range: {}", + find_closest_power_of_minus_two_str(&stat_diff) + ); + + assert_eq!(stat_diff, stat_diff_from_book(&rand_mod, &order)); + } } /// Given 0 < x < 1, returns `lo`, `hi` such that 2^-lo <= x <= 2^-hi @@ -96,3 +138,12 @@ fn find_closest_power_of_minus_two_str(x: &rug::Rational) -> String { let (lo, hi) = find_closest_power_of_minus_two(x); format!("2^-{lo}..2^-{hi}") } + +/// Formula taken from some book. I have no idea how it works, but we check if it produces the same result +/// as the other formula which is more understandable but less compact +fn stat_diff_from_book(a: &rug::Integer, b: &rug::Integer) -> rug::Rational { + let c = a.div_ceil(b).complete(); + let f = a.div_floor(b).complete(); + + (rug::Rational::from((&c, a)) - rug::Rational::from((1, b))) * (a - (&f * b).complete()) +} From bc7513fb3119d6c0a6eef54f925b2c8715c64fa6 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 21 Nov 2024 13:23:05 +0100 Subject: [PATCH 3/8] Restructure the lib, make HD derivations optional Signed-off-by: Denis Varlakov --- Cargo.toml | 25 +++-- src/edwards.rs | 110 +++++++++++++++++++ src/lib.rs | 292 ++----------------------------------------------- src/slip10.rs | 184 ++++++++++++++++++++++++++++++- 4 files changed, 319 insertions(+), 292 deletions(-) create mode 100644 src/edwards.rs diff --git a/Cargo.toml b/Cargo.toml index 7b437b1..a29376e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/LFDT-Lockness/hd-wallet" [dependencies] generic-ec = { version = "0.4", default-features = false } -hmac = { version = "0.12", default-features = false } -sha2 = { version = "0.10", default-features = false } -subtle = { version = "2", default-features = false } -generic-array = "0.14" +hmac = { version = "0.12", default-features = false, optional = true } +sha2 = { version = "0.10", default-features = false, optional = true } +subtle = { version = "2", default-features = false, optional = true } +generic-array = { version = "0.14", default-features = false, optional = true } serde = { version = "1", default-features = false, features = ["derive"], optional = true } @@ -27,13 +27,24 @@ rug = "1.26" generic-ec = { version = "0.4", default-features = false, features = ["curve-stark"] } [features] +default = [] std = [] -curve-secp256k1 = ["generic-ec/curve-secp256k1"] -curve-secp256r1 = ["generic-ec/curve-secp256r1"] -curve-ed25519 = ["generic-ec/curve-ed25519"] +# Adds support of secp256k1 curve to slip10 derivation +curve-secp256k1 = ["generic-ec/curve-secp256k1", "slip10"] +# Adds support of secp256r1 curve to slip10 derivation +curve-secp256r1 = ["generic-ec/curve-secp256r1", "slip10"] +# Enables Edwards-specific derivation +curve-ed25519 = ["generic-ec/curve-ed25519", "edwards"] all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-ed25519"] serde = ["dep:serde", "generic-ec/serde"] +# Enables Slip10 derivation +slip10 = ["hmac", "sha2", "generic-array", "subtle"] +# Enables Edwards-specific derivation +edwards = ["hmac", "sha2", "generic-array", "curve-ed25519"] +# Enables Stark-specific derivation +stark = [] + [[test]] name = "slip10_test_vector" required-features = ["curve-secp256k1", "curve-secp256r1"] diff --git a/src/edwards.rs b/src/edwards.rs new file mode 100644 index 0000000..bef64be --- /dev/null +++ b/src/edwards.rs @@ -0,0 +1,110 @@ +//! Edwards HD derivation +//! +//! This module provides [`Edwards`] derivation as well as aliases for calling +//! `>::*` methods for convenience when you don't need to support +//! generic HD derivation algorithm. +//! +//! See [`Edwards`] 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 Ed25519 curve +/// +/// This type of derivation isn't defined in any known to us standards, but it can be often +/// found in other libraries. It is secure and efficient (much more efficient than using +/// [`Slip10Like`](Slip10Like), for instance). +/// +/// ## Example +/// ```rust +/// use hd_wallet::{HdWallet, Edwards, curves::Ed25519}; +/// +/// # fn load_key() -> hd_wallet::ExtendedKeyPair { +/// # hd_wallet::ExtendedSecretKey { +/// # secret_key: generic_ec::SecretScalar::random(&mut rand::rngs::OsRng), +/// # chain_code: rand::Rng::gen(&mut rand::rngs::OsRng), +/// # }.into() +/// # } +/// # +/// let parent_key: hd_wallet::ExtendedKeyPair = load_key(); +/// +/// let child_key_pair = Edwards::derive_child_key_pair_with_path( +/// &parent_key, +/// [1 + hd_wallet::H, 10], +/// ); +/// # Ok::<(), Box>(()) +/// ``` +pub struct Edwards; + +impl DeriveShift for Edwards { + 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 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]) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(parent_public_key, i) + } + + 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 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()) + .finalize() + .into_bytes(); + Self::calculate_shift(&parent_key.public_key, i) + } +} + +impl Edwards { + fn calculate_shift( + parent_public_key: &ExtendedPublicKey, + i: hmac::digest::Output, + ) -> DerivedShift { + let (i_left, i_right) = split_into_two_halves(&i); + + let shift = Scalar::from_be_bytes_mod_order(i_left); + let child_pk = parent_public_key.public_key + Point::generator() * shift; + + DerivedShift { + shift, + child_public_key: ExtendedPublicKey { + public_key: child_pk, + chain_code: (*i_right).into(), + }, + } + } +} + +/// Splits array `I` of 64 bytes into two arrays `I_L = I[..32]` and `I_R = I[32..]` +fn split_into_two_halves( + i: &generic_array::GenericArray, +) -> ( + &generic_array::GenericArray, + &generic_array::GenericArray, +) { + generic_array::sequence::Split::split(i) +} + +super::create_aliases!(Edwards, edwards, hd_wallet::curves::Ed25519); diff --git a/src/lib.rs b/src/lib.rs index 7ee9f68..696d471 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,44 +67,24 @@ #![cfg_attr(not(feature = "std"), no_std)] #![forbid(missing_docs, unsafe_code)] +#![cfg_attr(not(test), forbid(unused_crate_dependencies))] use core::ops; -use generic_array::{ - typenum::{U32, U64}, - GenericArray, -}; use generic_ec::{Curve, Point, Scalar, SecretScalar}; -use hmac::Mac; pub use generic_ec::curves; +#[cfg(feature = "edwards")] +pub mod edwards; pub mod errors; +#[cfg(feature = "slip10")] pub mod slip10; -/// Slip10-like HD derivation -/// -/// This module provides aliases for calling `>::*` methods for convenience -/// when you don't need to support generic HD derivation algorithm. -/// -/// See [`Slip10Like`] docs to learn more about the derivation method. -pub mod slip10_like { - pub use crate::Slip10Like; - super::create_aliases!(Slip10Like, slip10_like); -} - -/// Edwards HD derivation -/// -/// This module provides aliases for calling `>::*` methods for convenience -/// when you don't need to support generic HD derivation algorithm. -/// -/// See [`Edwards`] docs to learn more about the derivation method. -pub mod edwards { - pub use crate::Edwards; - super::create_aliases!(Edwards, edwards, hd_wallet::curves::Ed25519); -} - -type HmacSha512 = hmac::Hmac; +#[cfg(feature = "edwards")] +pub use edwards::Edwards; +#[cfg(feature = "slip10")] +pub use slip10::Slip10; /// Beginning of hardened child indexes /// @@ -369,6 +349,7 @@ impl<'de, E: Curve> serde::Deserialize<'de> for ExtendedKeyPair { /// * `$m` - current module, module where these functions will appear. Used in doc /// tests only /// * `$e` - curve supported by this HD derivation, used in doc tests only +#[cfg(any(feature = "slip10", feature = "edwards", feature = "stark"))] macro_rules! create_aliases { ($t:ty, $m:expr) => { $crate::create_aliases!($t, $m, hd_wallet::curves::Secp256k1); }; ($t:ty, $m:expr, $e:ty) => { @@ -604,6 +585,7 @@ macro_rules! create_aliases { } }; } +#[cfg(any(feature = "slip10", feature = "edwards", feature = "stark"))] pub(crate) use create_aliases; /// HD derivation @@ -830,257 +812,3 @@ pub trait DeriveShift { child_index: HardenedIndex, ) -> DerivedShift; } - -/// SLIP10-like HD wallet derivation -/// -/// `Slip10Like` is generalization of [`Slip10`], which is defined for any curve that meets -/// constraints listed below. -/// -/// When `Slip10Like` is instantiated with secp256k1 or secp256r1 curves, it follows exactly -/// SLIP10 derivation rules. -/// -/// ## Constraints -/// `Slip10Like` must be used with curves which operate on 32 bytes scalars. -/// -/// `Slip10Like` is not recommended to be used with curves with order significantly lower -/// than $2^{256}$ (e.g. ed25519) as it worsens the performance. -/// -/// ### Ed25519 curve -/// Although `Slip10Like` will work on ed25519 curve, we do not recommend using it, because: -/// 1. it's confusing as ed25519 curve is defined in SLIP10, however, -/// `Slip10Like` will not follow SLIP10 standard -/// 2. it's quite inefficient -/// -/// Prefer using [`Edwards`] derivation method for ed25519 curve. -pub struct Slip10Like; - -impl DeriveShift for Slip10Like { - 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 i = hmac - .clone() - .chain_update(parent_public_key.public_key.to_bytes(true)) - .chain_update(child_index.to_be_bytes()) - .finalize() - .into_bytes(); - Self::calculate_shift(&hmac, parent_public_key, *child_index, i) - } - - 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 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()) - .finalize() - .into_bytes(); - Self::calculate_shift(&hmac, &parent_key.public_key, *child_index, i) - } -} - -impl Slip10Like { - fn calculate_shift( - hmac: &HmacSha512, - parent_public_key: &ExtendedPublicKey, - child_index: u32, - mut i: hmac::digest::Output, - ) -> DerivedShift { - loop { - let (i_left, i_right) = split_into_two_halves(&i); - - if let Ok(shift) = Scalar::::from_be_bytes(i_left) { - let child_pk = parent_public_key.public_key + Point::generator() * shift; - if !child_pk.is_zero() { - return DerivedShift { - shift, - child_public_key: ExtendedPublicKey { - public_key: child_pk, - chain_code: (*i_right).into(), - }, - }; - } - } - - i = hmac - .clone() - .chain_update([0x01]) - .chain_update(i_right) - .chain_update(child_index.to_be_bytes()) - .finalize() - .into_bytes() - } - } -} - -/// [SLIP10][slip10-spec] HD wallet derivation -/// -/// Performs HD derivation as defined in the spec. Only supports secp256k1 and secp256r1 curves. -/// -/// ## Limitations -/// We do not support SLIP10 instantiated with ed25519 or curve25519 due to the limitations. -/// Ed25519 and curve25519 are special-cases in SLIP10 standard, they only support hardened -/// derivation, and they operate on EdDSA and X25519 private keys instead of elliptic points -/// and scalars as in other cases. This library only supports HD derivations in which -/// 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. -/// -/// ## Master key derivation from the seed -/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined -/// in the spec. -/// -/// ## Example -/// Derive a master key from the seed, and then derive a child key m/1H/10: -/// ```rust -/// use hd_wallet::{HdWallet, Slip10, curves::Secp256k1}; -/// -/// let seed = b"16-64 bytes of high entropy".as_slice(); -/// let master_key = hd_wallet::slip10::derive_master_key::(seed)?; -/// let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); -/// -/// let child_key_pair = Slip10::derive_child_key_pair_with_path( -/// &master_key_pair, -/// [1 + hd_wallet::H, 10], -/// ); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ## SLIP10-like derivation -/// SLIP10 is only defined for a few curves, but it can be extended to support any curve. -/// See [`Slip10Like`] if you need other curves than is supported by SLIP10. -/// -/// [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md -pub struct Slip10; - -#[cfg(feature = "curve-secp256k1")] -impl DeriveShift for Slip10 { - fn derive_public_shift( - parent_public_key: &ExtendedPublicKey, - child_index: NonHardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_public_shift(parent_public_key, child_index) - } - fn derive_hardened_shift( - parent_key: &ExtendedKeyPair, - child_index: HardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_hardened_shift(parent_key, child_index) - } -} -#[cfg(feature = "curve-secp256r1")] -impl DeriveShift for Slip10 { - fn derive_public_shift( - parent_public_key: &ExtendedPublicKey, - child_index: NonHardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_public_shift(parent_public_key, child_index) - } - fn derive_hardened_shift( - parent_key: &ExtendedKeyPair, - child_index: HardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_hardened_shift(parent_key, child_index) - } -} - -/// Splits array `I` of 64 bytes into two arrays `I_L = I[..32]` and `I_R = I[32..]` -fn split_into_two_halves( - i: &GenericArray, -) -> (&GenericArray, &GenericArray) { - generic_array::sequence::Split::split(i) -} - -/// HD derivation for Ed25519 curve -/// -/// This type of derivation isn't defined in any known to us standards, but it can be often -/// found in other libraries. It is secure and efficient (much more efficient than using -/// [`Slip10Like`](Slip10Like), for instance). -/// -/// ## Example -/// ```rust -/// use hd_wallet::{HdWallet, Edwards, curves::Ed25519}; -/// -/// # fn load_key() -> hd_wallet::ExtendedKeyPair { -/// # hd_wallet::ExtendedSecretKey { -/// # secret_key: generic_ec::SecretScalar::random(&mut rand::rngs::OsRng), -/// # chain_code: rand::Rng::gen(&mut rand::rngs::OsRng), -/// # }.into() -/// # } -/// # -/// let parent_key: hd_wallet::ExtendedKeyPair = load_key(); -/// -/// let child_key_pair = Edwards::derive_child_key_pair_with_path( -/// &parent_key, -/// [1 + hd_wallet::H, 10], -/// ); -/// # Ok::<(), Box>(()) -/// ``` -pub struct Edwards; - -#[cfg(feature = "curve-ed25519")] -impl DeriveShift for Edwards { - 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 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]) - .chain_update(child_index.to_be_bytes()) - .finalize() - .into_bytes(); - Self::calculate_shift(parent_public_key, i) - } - - 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 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()) - .finalize() - .into_bytes(); - Self::calculate_shift(&parent_key.public_key, i) - } -} - -#[cfg(feature = "curve-ed25519")] -impl Edwards { - fn calculate_shift( - parent_public_key: &ExtendedPublicKey, - i: hmac::digest::Output, - ) -> DerivedShift { - let (i_left, i_right) = split_into_two_halves(&i); - - let shift = Scalar::from_be_bytes_mod_order(i_left); - let child_pk = parent_public_key.public_key + Point::generator() * shift; - - DerivedShift { - shift, - child_public_key: ExtendedPublicKey { - public_key: child_pk, - chain_code: (*i_right).into(), - }, - } - } -} diff --git a/src/slip10.rs b/src/slip10.rs index 2db3ee9..c0b88ff 100644 --- a/src/slip10.rs +++ b/src/slip10.rs @@ -12,9 +12,187 @@ //! [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md //! [bip32-spec]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +use generic_ec::{Curve, Point, Scalar}; use hmac::Mac as _; -pub use crate::Slip10; +use crate::{ + DeriveShift, DerivedShift, ExtendedKeyPair, ExtendedPublicKey, HardenedIndex, NonHardenedIndex, +}; + +type HmacSha512 = hmac::Hmac; + +/// SLIP10-like HD wallet derivation +/// +/// `Slip10Like` is generalization of [`Slip10`], which is defined for any curve that meets +/// constraints listed below. +/// +/// When `Slip10Like` is instantiated with secp256k1 or secp256r1 curves, it follows exactly +/// SLIP10 derivation rules. +/// +/// ## Constraints +/// `Slip10Like` must be used with curves which operate on 32 bytes scalars. +/// +/// `Slip10Like` is not recommended to be used with curves with order significantly lower +/// than $2^{256}$ (e.g. ed25519) as it worsens the performance. +/// +/// ### Ed25519 curve +/// Although `Slip10Like` will work on ed25519 curve, we do not recommend using it, because: +/// 1. it's confusing as ed25519 curve is defined in SLIP10, however, +/// `Slip10Like` will not follow SLIP10 standard +/// 2. it's quite inefficient +/// +/// Prefer using [`Edwards`] derivation method for ed25519 curve. +pub struct Slip10Like; + +impl DeriveShift for Slip10Like { + 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 i = hmac + .clone() + .chain_update(parent_public_key.public_key.to_bytes(true)) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes(); + Self::calculate_shift(&hmac, parent_public_key, *child_index, i) + } + + 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 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()) + .finalize() + .into_bytes(); + Self::calculate_shift(&hmac, &parent_key.public_key, *child_index, i) + } +} + +impl Slip10Like { + fn calculate_shift( + hmac: &HmacSha512, + parent_public_key: &ExtendedPublicKey, + child_index: u32, + mut i: hmac::digest::Output, + ) -> DerivedShift { + loop { + let (i_left, i_right) = split_into_two_halves(&i); + + if let Ok(shift) = Scalar::::from_be_bytes(i_left) { + let child_pk = parent_public_key.public_key + Point::generator() * shift; + if !child_pk.is_zero() { + return DerivedShift { + shift, + child_public_key: ExtendedPublicKey { + public_key: child_pk, + chain_code: (*i_right).into(), + }, + }; + } + } + + i = hmac + .clone() + .chain_update([0x01]) + .chain_update(i_right) + .chain_update(child_index.to_be_bytes()) + .finalize() + .into_bytes() + } + } +} + +/// Splits array `I` of 64 bytes into two arrays `I_L = I[..32]` and `I_R = I[32..]` +fn split_into_two_halves( + i: &generic_array::GenericArray, +) -> ( + &generic_array::GenericArray, + &generic_array::GenericArray, +) { + generic_array::sequence::Split::split(i) +} + +/// [SLIP10][slip10-spec] HD wallet derivation +/// +/// Performs HD derivation as defined in the spec. Only supports secp256k1 and secp256r1 curves. +/// +/// ## Limitations +/// We do not support SLIP10 instantiated with ed25519 or curve25519 due to the limitations. +/// Ed25519 and curve25519 are special-cases in SLIP10 standard, they only support hardened +/// derivation, and they operate on EdDSA and X25519 private keys instead of elliptic points +/// and scalars as in other cases. This library only supports HD derivations in which +/// 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. +/// +/// ## Master key derivation from the seed +/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined +/// in the spec. +/// +/// ## Example +/// Derive a master key from the seed, and then derive a child key m/1H/10: +/// ```rust +/// use hd_wallet::{HdWallet, Slip10, curves::Secp256k1}; +/// +/// let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = hd_wallet::slip10::derive_master_key::(seed)?; +/// let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); +/// +/// let child_key_pair = Slip10::derive_child_key_pair_with_path( +/// &master_key_pair, +/// [1 + hd_wallet::H, 10], +/// ); +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ## SLIP10-like derivation +/// SLIP10 is only defined for a few curves, but it can be extended to support any curve. +/// See [`Slip10Like`] if you need other curves than is supported by SLIP10. +/// +/// [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md +pub struct Slip10; + +#[cfg(feature = "curve-secp256k1")] +impl DeriveShift for Slip10 { + fn derive_public_shift( + parent_public_key: &ExtendedPublicKey, + child_index: NonHardenedIndex, + ) -> DerivedShift { + Slip10Like::derive_public_shift(parent_public_key, child_index) + } + fn derive_hardened_shift( + parent_key: &ExtendedKeyPair, + child_index: HardenedIndex, + ) -> DerivedShift { + Slip10Like::derive_hardened_shift(parent_key, child_index) + } +} +#[cfg(feature = "curve-secp256r1")] +impl DeriveShift for Slip10 { + fn derive_public_shift( + parent_public_key: &ExtendedPublicKey, + child_index: NonHardenedIndex, + ) -> DerivedShift { + Slip10Like::derive_public_shift(parent_public_key, child_index) + } + fn derive_hardened_shift( + parent_key: &ExtendedKeyPair, + child_index: HardenedIndex, + ) -> DerivedShift { + Slip10Like::derive_hardened_shift(parent_key, child_index) + } +} /// Marker for a curve supported by SLIP10 specs and this library /// @@ -79,12 +257,12 @@ pub fn derive_master_key_with_curve_tag( return Err(crate::errors::InvalidLength); } - let hmac = crate::HmacSha512::new_from_slice(curve_tag) + let hmac = HmacSha512::new_from_slice(curve_tag) .expect("this never fails: hmac can handle keys of any size"); let mut i = hmac.clone().chain_update(seed).finalize().into_bytes(); loop { - let (i_left, i_right) = crate::split_into_two_halves(&i); + let (i_left, i_right) = split_into_two_halves(&i); if let Ok(mut sk) = generic_ec::Scalar::::from_be_bytes(i_left) { if !bool::from(subtle::ConstantTimeEq::ct_eq( From f1521088b748ca3932c47f1f9aabb2753ebf4223 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 21 Nov 2024 13:31:29 +0100 Subject: [PATCH 4/8] Remove slip10-like derivation Signed-off-by: Denis Varlakov --- src/edwards.rs | 3 +- src/lib.rs | 4 +- src/slip10.rs | 154 +++++++++++++++--------------------- tests/slip10_test_vector.rs | 2 +- 4 files changed, 67 insertions(+), 96 deletions(-) diff --git a/src/edwards.rs b/src/edwards.rs index bef64be..f040122 100644 --- a/src/edwards.rs +++ b/src/edwards.rs @@ -18,8 +18,7 @@ type HmacSha512 = hmac::Hmac; /// HD derivation for Ed25519 curve /// /// This type of derivation isn't defined in any known to us standards, but it can be often -/// found in other libraries. It is secure and efficient (much more efficient than using -/// [`Slip10Like`](Slip10Like), for instance). +/// found in other libraries. It is secure and efficient. /// /// ## Example /// ```rust diff --git a/src/lib.rs b/src/lib.rs index 696d471..91bb8ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ //! //! [`HdWallet`] trait generalizes HD derivation algorithm, you can use it with generics: //! ```rust -//! use hd_wallet::{Slip10Like, curves::Secp256r1}; +//! use hd_wallet::{Slip10, curves::Secp256r1}; //! //! fn derive_using_generic_algo>( //! master_key: hd_wallet::ExtendedKeyPair, @@ -48,7 +48,7 @@ //! let seed = b"16-64 bytes of high entropy".as_slice(); //! let master_key = hd_wallet::slip10::derive_master_key(seed)?; //! let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); -//! let child_key = derive_using_generic_algo::(master_key_pair); +//! let child_key = derive_using_generic_algo::(master_key_pair); //! //! # Ok::<(), Box>(()) //! ``` diff --git a/src/slip10.rs b/src/slip10.rs index c0b88ff..a8acb3e 100644 --- a/src/slip10.rs +++ b/src/slip10.rs @@ -1,7 +1,7 @@ //! SLIP10 derivation //! -//! [SLIP10][slip10-spec] is a specification for implementing HD wallets. It aims at supporting many -//! curves while being compatible with [BIP32][bip32-spec]. +//! [SLIP10][slip10-spec] is a specification for implementing HD wallets. It aims at supporting more +//! curves than [BIP32][bip32-spec] while being compatible with it. //! //! Refer to [`Slip10`] docs to learn more about the derivation method. //! @@ -21,31 +21,53 @@ use crate::{ type HmacSha512 = hmac::Hmac; -/// SLIP10-like HD wallet derivation +/// [SLIP10][slip10-spec] HD wallet derivation /// -/// `Slip10Like` is generalization of [`Slip10`], which is defined for any curve that meets -/// constraints listed below. +/// Performs HD derivation as defined in the spec. Only supports secp256k1 and secp256r1 curves. /// -/// When `Slip10Like` is instantiated with secp256k1 or secp256r1 curves, it follows exactly -/// SLIP10 derivation rules. +/// ## Limitations +/// We do not support SLIP10 instantiated with ed25519 or curve25519 due to the limitations. +/// Ed25519 and curve25519 are special-cases in SLIP10 standard, they only support hardened +/// derivation, and they operate on EdDSA and X25519 private keys instead of elliptic points +/// and scalars as in other cases. This library only supports HD derivations in which +/// 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. /// -/// ## Constraints -/// `Slip10Like` must be used with curves which operate on 32 bytes scalars. +/// ## Master key derivation from the seed +/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined +/// in the spec. /// -/// `Slip10Like` is not recommended to be used with curves with order significantly lower -/// than $2^{256}$ (e.g. ed25519) as it worsens the performance. +/// ## Example +/// Derive a master key from the seed, and then derive a child key m/1H/10: +/// ```rust +/// use hd_wallet::{HdWallet, Slip10, curves::Secp256k1}; /// -/// ### Ed25519 curve -/// Although `Slip10Like` will work on ed25519 curve, we do not recommend using it, because: -/// 1. it's confusing as ed25519 curve is defined in SLIP10, however, -/// `Slip10Like` will not follow SLIP10 standard -/// 2. it's quite inefficient +/// let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = hd_wallet::slip10::derive_master_key::(seed)?; +/// let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); /// -/// Prefer using [`Edwards`] derivation method for ed25519 curve. -pub struct Slip10Like; +/// let child_key_pair = Slip10::derive_child_key_pair_with_path( +/// &master_key_pair, +/// [1 + hd_wallet::H, 10], +/// ); +/// # Ok::<(), Box>(()) +/// ``` +/// +/// [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md +pub struct Slip10; -impl DeriveShift for Slip10Like { - fn derive_public_shift( +impl Slip10 { + /// Derives public shift for any curve, regardless whether it's in the slip10 spec + /// or not + /// + /// DO NOT use it with curves not specified in slip10. Other curves might be subject to + /// DoS attack: attacker may find inputs to HD derivation which would result in a lot of + /// iterations of HMAC-ing. Only curves from the spec are proven to be resistant to this + /// attack. + fn derive_public_shift_for_any_curve( parent_public_key: &ExtendedPublicKey, child_index: NonHardenedIndex, ) -> DerivedShift { @@ -57,10 +79,17 @@ impl DeriveShift for Slip10Like { .chain_update(child_index.to_be_bytes()) .finalize() .into_bytes(); - Self::calculate_shift(&hmac, parent_public_key, *child_index, i) + Self::calculate_shift_for_any_curve(&hmac, parent_public_key, *child_index, i) } - fn derive_hardened_shift( + /// Derives hardened shift for any curve, regardless whether it's in the slip10 spec + /// or not + /// + /// DO NOT use it with curves not specified in slip10. Other curves might be subject to + /// DoS attack: attacker may find inputs to HD derivation which would result in a lot of + /// iterations of HMAC-ing. Only curves from the spec are proven to be resistant to this + /// attack. + fn derive_hardened_shift_for_any_curve( parent_key: &ExtendedKeyPair, child_index: HardenedIndex, ) -> DerivedShift { @@ -73,12 +102,10 @@ impl DeriveShift for Slip10Like { .chain_update(child_index.to_be_bytes()) .finalize() .into_bytes(); - Self::calculate_shift(&hmac, &parent_key.public_key, *child_index, i) + Self::calculate_shift_for_any_curve(&hmac, &parent_key.public_key, *child_index, i) } -} -impl Slip10Like { - fn calculate_shift( + fn calculate_shift_for_any_curve( hmac: &HmacSha512, parent_public_key: &ExtendedPublicKey, child_index: u32, @@ -121,76 +148,21 @@ fn split_into_two_halves( generic_array::sequence::Split::split(i) } -/// [SLIP10][slip10-spec] HD wallet derivation -/// -/// Performs HD derivation as defined in the spec. Only supports secp256k1 and secp256r1 curves. -/// -/// ## Limitations -/// We do not support SLIP10 instantiated with ed25519 or curve25519 due to the limitations. -/// Ed25519 and curve25519 are special-cases in SLIP10 standard, they only support hardened -/// derivation, and they operate on EdDSA and X25519 private keys instead of elliptic points -/// and scalars as in other cases. This library only supports HD derivations in which -/// 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. -/// -/// ## Master key derivation from the seed -/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined -/// in the spec. -/// -/// ## Example -/// Derive a master key from the seed, and then derive a child key m/1H/10: -/// ```rust -/// use hd_wallet::{HdWallet, Slip10, curves::Secp256k1}; -/// -/// let seed = b"16-64 bytes of high entropy".as_slice(); -/// let master_key = hd_wallet::slip10::derive_master_key::(seed)?; -/// let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); -/// -/// let child_key_pair = Slip10::derive_child_key_pair_with_path( -/// &master_key_pair, -/// [1 + hd_wallet::H, 10], -/// ); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ## SLIP10-like derivation -/// SLIP10 is only defined for a few curves, but it can be extended to support any curve. -/// See [`Slip10Like`] if you need other curves than is supported by SLIP10. -/// -/// [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md -pub struct Slip10; - -#[cfg(feature = "curve-secp256k1")] -impl DeriveShift for Slip10 { +impl DeriveShift for Slip10 +where + E: Curve + SupportedCurve, +{ fn derive_public_shift( - parent_public_key: &ExtendedPublicKey, - child_index: NonHardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_public_shift(parent_public_key, child_index) - } - fn derive_hardened_shift( - parent_key: &ExtendedKeyPair, - child_index: HardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_hardened_shift(parent_key, child_index) - } -} -#[cfg(feature = "curve-secp256r1")] -impl DeriveShift for Slip10 { - fn derive_public_shift( - parent_public_key: &ExtendedPublicKey, + parent_public_key: &ExtendedPublicKey, child_index: NonHardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_public_shift(parent_public_key, child_index) + ) -> DerivedShift { + Slip10::derive_public_shift_for_any_curve(parent_public_key, child_index) } fn derive_hardened_shift( - parent_key: &ExtendedKeyPair, + parent_key: &ExtendedKeyPair, child_index: HardenedIndex, - ) -> DerivedShift { - Slip10Like::derive_hardened_shift(parent_key, child_index) + ) -> DerivedShift { + Slip10::derive_hardened_shift_for_any_curve(parent_key, child_index) } } diff --git a/tests/slip10_test_vector.rs b/tests/slip10_test_vector.rs index c8cdb15..83c41a7 100644 --- a/tests/slip10_test_vector.rs +++ b/tests/slip10_test_vector.rs @@ -347,7 +347,7 @@ fn run_vector(v: &TestVector) { let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); for derivation in v.derivations { - let key = hd_wallet::Slip10Like::derive_child_key_pair_with_path( + let key = hd_wallet::Slip10::derive_child_key_pair_with_path( &master_key_pair, derivation.path.iter().copied(), ); From 9bd5655db3258419a90928b1d156e3d2e4044dc1 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 25 Nov 2024 12:37:27 +0100 Subject: [PATCH 5/8] 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) + ); + } + } +} From 7ccea272b9d71ff3028c5cb8c282116ea889dc4c Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 25 Nov 2024 12:43:14 +0100 Subject: [PATCH 6/8] Update workflow Signed-off-by: Denis Varlakov --- .github/workflows/rust.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0cf589c..54f59e8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,7 +9,7 @@ env: CARGO_NET_GIT_FETCH_WITH_CLI: true jobs: - build-no-features: + check-no-features: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -17,16 +17,33 @@ jobs: with: cache-on-failure: "true" - name: Build - run: cargo build - build-and-test-all-features: + run: cargo check --no-default-features + check: runs-on: ubuntu-latest + strategy: + matrix: + features: + - slip10 + - slip10,curve-secp256k1 + - slip10,curve-secp256r1 + - edwards + - stark steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" - name: Build - run: cargo build --all-features + run: cargo check --features ${{ matrix.features }} + check-and-test-all-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Build + run: cargo check --all-features - name: Run tests run: cargo test --all-features --lib --tests doctest: From 36a4130123c83af8119928ba288323e7c5dfd39a Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 25 Nov 2024 12:46:23 +0100 Subject: [PATCH 7/8] Update readme Signed-off-by: Denis Varlakov --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1a0b64..c367fee 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ let child_key_pair = slip10::derive_child_key_pair_with_path( `HdWallet` trait generalizes HD derivation algorithm, you can use it with generics: ```rust -use hd_wallet::{Slip10Like, curves::Secp256r1}; +use hd_wallet::{Slip10, curves::Secp256r1}; fn derive_using_generic_algo>( master_key: hd_wallet::ExtendedKeyPair, @@ -47,7 +47,7 @@ fn derive_using_generic_algo>( let seed = b"16-64 bytes of high entropy".as_slice(); let master_key = hd_wallet::slip10::derive_master_key(seed)?; let master_key_pair = hd_wallet::ExtendedKeyPair::from(master_key); -let child_key = derive_using_generic_algo::(master_key_pair); +let child_key = derive_using_generic_algo::(master_key_pair); ``` From 158b65ce54ed6d0d031a9414890b4a9170dd4f11 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 25 Nov 2024 12:53:18 +0100 Subject: [PATCH 8/8] Update changelog Signed-off-by: Denis Varlakov --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ea7f9..04d73e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # hd-wallet crate changelog +## v0.6.0 +* Remove slip10-like derivation that can be instantiated with any curve: it is very inefficient + when instantiated with certain curves, and may also enable attacker to perform DoS attack by + finding derivation path that results into very long computation [#14] +* Add stark-specific derivation: secure and efficient derivation for stark curve [#14] + +[#14]: https://github.com/LFDT-Lockness/hd-wallet/pull/14 + ## v0.5.1 * Update docs and repo link [#11] diff --git a/Cargo.lock b/Cargo.lock index 349cae9..75c3df4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "hd-wallet" -version = "0.5.1" +version = "0.6.0" dependencies = [ "generic-array", "generic-ec", diff --git a/Cargo.toml b/Cargo.toml index f4c7217..71bc57b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hd-wallet" -version = "0.5.1" +version = "0.6.0" edition = "2021" license = "MIT OR Apache-2.0" description = "HD wallets derivation"