diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7b940b7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,22 @@ +on: + push: + tags: + - 'v*' + workflow_dispatch: + +name: Publish + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + publish-round-based: + name: Publish crate + environment: crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml new file mode 100644 index 0000000..4907af8 --- /dev/null +++ b/.github/workflows/readme.yml @@ -0,0 +1,24 @@ +name: Check README + +on: + pull_request: + branches: [ "*" ] + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + check_readme: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install cargo-hakari + uses: baptiste0928/cargo-install@v1 + with: + crate: cargo-readme + - name: Check that readme matches lib.rs + run: | + cp README.md README-copy.md + make readme + diff README.md README-copy.md \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..b76a34b --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,64 @@ +name: Rust + +on: + pull_request: + branches: [ "*" ] + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + build-no-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Build + run: cargo build + build-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 build --all-features + - name: Run tests + run: cargo test --all-features --lib --tests + doctest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Doctests + run: cargo test --doc --all-features + check-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check formatting + run: cargo fmt --all -- --check + check-doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Check docs + run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps + check-clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Run clippy + run: cargo clippy -- -D clippy::all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7fb4a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target + +.helix/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..82a66b6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,528 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-ec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81960ce6c780f5a63f6ab4e94b3b34212f839ce7e7768b953413e4ee3c4d1438" +dependencies = [ + "generic-ec-core", + "generic-ec-curves", + "getrandom", + "phantom-type", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1193740c17f0324ea5d7db0635230f7538e5d33e0f9732af92d3f9fd4b87663e" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-curves" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0230c551d3f3c0d272092e961de40cdaf7661ad5560948f6a262b64cd9bc2" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "generic-ec-core", + "k256", + "p256", + "rand_core", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "k256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "phantom-type" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68f5dc797c2a743e024e1c53215474598faf0408826a90249569ad7f47adeaa" +dependencies = [ + "educe", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slip-10" +version = "0.1.0" +dependencies = [ + "generic-array", + "generic-ec", + "hex-literal", + "hmac", + "sha2", + "subtle", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c25a2ae --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "slip-10" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +generic-ec = { version = "0.1", 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" + +[dev-dependencies] +hex-literal = "0.4" + +[features] +std = [] +curve-secp256k1 = ["generic-ec/curve-secp256k1"] +curve-secp256r1 = ["generic-ec/curve-secp256r1"] +all-curves = ["curve-secp256k1", "curve-secp256r1"] + +[[test]] +name = "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/Makefile b/Makefile new file mode 100644 index 0000000..d562bd5 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: docs docs-open + +docs: + RUSTDOCFLAGS="--html-in-header katex-header.html" cargo +nightly doc --no-deps --all-features + +docs-open: + RUSTDOCFLAGS="--html-in-header katex-header.html" cargo +nightly doc --no-deps --all-features --open + +docs-private: + RUSTDOCFLAGS="--html-in-header katex-header.html" cargo +nightly doc --no-deps --all-features --document-private-items + +readme: + cargo readme -i src/lib.rs --no-indent-headings \ + | perl -ne 's/(? README.md diff --git a/README.md b/README.md index 8b13789..c3614d2 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ +# SLIP-10: Deterministic key generation +[SLIP10][slip10-spec] is a specification for implementing HD wallets. It aims at supporting many +curves while being compatible with [BIP32][bip32-spec]. + +The implementation is based on generic-ec library that provides generic +elliptic curve arithmetic. The crate is `no_std` and `no_alloc` friendly. + +### Curves support +Implementation currently does not support ed25519 curve. All other curves are +supported: both secp256k1 and secp256r1. In fact, implementation may work with any +curve, but only those are covered by the SLIP10 specs. + +The crate also re-exports supported curves in supported_curves module (requires +enabling a feature), but any other curve implementation will work with the crate. + +### Features +* `std`: enables std library support (mainly, it just implements `Error` + trait for the error types) +* `curve-secp256k1` and `curve-secp256r1` add curve implementation into the crate supported_curves + module + +### Examples + +Derive a master key from the seed, and then derive a child key m/1H/10: +```rust +use slip_10::supported_curves::Secp256k1; + +let seed = b"16-64 bytes of high entropy".as_slice(); +let master_key = slip_10::derive_master_key::(seed)?; +let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); + +let child_key_pair = slip_10::derive_child_key_pair_with_path( + &master_key_pair, + [1 + slip_10::H, 10], +); +``` + +[slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md +[bip32-spec]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..37dc16b --- /dev/null +++ b/README.tpl @@ -0,0 +1 @@ +# {{readme}} \ No newline at end of file diff --git a/katex-header.html b/katex-header.html new file mode 100644 index 0000000..9d8f376 --- /dev/null +++ b/katex-header.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..4c003e3 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,57 @@ +//! When something goes wrong + +use core::fmt; + +/// Length of the argument is not valid +#[derive(Debug)] +pub struct InvalidLength; + +impl fmt::Display for InvalidLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid length") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InvalidLength {} + +/// Value was out of range +#[derive(Debug)] +pub struct OutOfRange; + +impl fmt::Display for OutOfRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("out of range") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for OutOfRange {} + +/// Error returned by parsing child index +#[derive(Debug)] +pub enum ParseChildIndexError { + /// Indicates that parsing an `u32` integer failed + ParseInt(core::num::ParseIntError), + /// Parsed index was out of acceptable range + IndexNotInRange(OutOfRange), +} + +impl fmt::Display for ParseChildIndexError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseInt(_) => f.write_str("child index is not valid u32 integer"), + Self::IndexNotInRange(_) => f.write_str("child index is not in acceptable range"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseChildIndexError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ParseChildIndexError::ParseInt(e) => Some(e), + ParseChildIndexError::IndexNotInRange(e) => Some(e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4002c39 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,607 @@ +//! SLIP-10: Deterministic key generation +//! +//! [SLIP10][slip10-spec] is a specification for implementing HD wallets. It aims at supporting many +//! curves while being compatible with [BIP32][bip32-spec]. +//! +//! The implementation is based on [generic-ec](generic_ec) library that provides generic +//! elliptic curve arithmetic. The crate is `no_std` and `no_alloc` friendly. +//! +//! ### Curves support +//! Implementation currently does not support ed25519 curve. All other curves are +//! supported: both secp256k1 and secp256r1. In fact, implementation may work with any +//! curve, but only those are covered by the SLIP10 specs. +//! +//! The crate also re-exports supported curves in [supported_curves] module (requires +//! enabling a feature), but any other curve implementation will work with the crate. +//! +//! ### Features +//! * `std`: enables std library support (mainly, it just implements [`Error`](std::error::Error) +//! trait for the error types) +//! * `curve-secp256k1` and `curve-secp256r1` add curve implementation into the crate [supported_curves] +//! module +//! +//! ### Examples +//! +//! Derive a master key from the seed, and then derive a child key m/1H/10: +//! ```rust +//! use slip_10::supported_curves::Secp256k1; +//! +//! let seed = b"16-64 bytes of high entropy".as_slice(); +//! let master_key = slip_10::derive_master_key::(seed)?; +//! let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); +//! +//! let child_key_pair = slip_10::derive_child_key_pair_with_path( +//! &master_key_pair, +//! [1 + slip_10::H, 10], +//! ); +//! # Ok::<(), Box>(()) +//! ``` +//! +//! [slip10-spec]: https://github.com/satoshilabs/slips/blob/master/slip-0010.md +//! [bip32-spec]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(missing_docs, unsafe_code)] + +use core::ops; + +use generic_array::{ + typenum::{U32, U64}, + GenericArray, +}; +use generic_ec::{Curve, Point, Scalar, SecretScalar}; +use hmac::Mac as _; + +#[cfg(any( + feature = "curve-secp256k1", + feature = "curve-secp256r1", + feature = "all-curves" +))] +pub use generic_ec::curves as supported_curves; + +pub mod errors; + +type HmacSha512 = hmac::Hmac; +/// Beggining of hardened child indexes +/// +/// $H = 2^{31}$ defines the range of hardened indexes. All indexes $i$ such that $H \le i$ are hardened. +/// +/// ## Example +/// Derive a child key with a path m/1H +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// +/// # let seed = b"do not use this seed in prod :)".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); +/// +/// let hardened_child = slip_10::derive_child_key_pair( +/// &master_key_pair, +/// 1 + slip_10::H, +/// ); +/// # +/// # Ok::<(), slip_10::errors::InvalidLength>(()) +/// ``` +pub const H: u32 = 1 << 31; + +/// Child index, whether hardened or not +#[derive(Clone, Copy, Debug)] +pub enum ChildIndex { + /// Hardened index + Hardened(HardenedIndex), + /// Non-hardened index + NonHardened(NonHardenedIndex), +} + +/// Child index in range $2^{31} \le i < 2^{32}$ corresponing to a hardened wallet +#[derive(Clone, Copy, Debug)] +pub struct HardenedIndex(u32); + +/// Child index in range $0 \le i < 2^{31}$ corresponing to a non-hardened wallet +#[derive(Clone, Copy, Debug)] +pub struct NonHardenedIndex(u32); + +/// Extended public key +#[derive(Clone, Copy, Debug)] +pub struct ExtendedPublicKey { + /// The public key that can be used for signature verification + pub public_key: Point, + /// A chain code that is used to derive child keys + pub chain_code: ChainCode, +} + +/// Extended secret key +#[derive(Clone, Debug)] +pub struct ExtendedSecretKey { + /// The secret key that can be used for signing + pub secret_key: SecretScalar, + /// A chain code that is used to derive child keys + pub chain_code: ChainCode, +} + +/// Pair of extended secret and public keys +#[derive(Clone, Debug)] +pub struct ExtendedKeyPair { + public_key: ExtendedPublicKey, + secret_key: ExtendedSecretKey, +} + +/// A shift that can be applied to parent key to obtain a child key +/// +/// It contains an already derived child public key as it needs to be derived +/// in process of calculating the shift value +#[derive(Clone, Copy, Debug)] +pub struct DerivedShift { + /// Derived shift + pub shift: Scalar, + /// Derived child extended public key + pub child_public_key: ExtendedPublicKey, +} + +/// Chain code of extended key as defined in SLIP-10 +pub type ChainCode = [u8; 32]; + +impl HardenedIndex { + /// The smallest possible value of hardened index. Equals to $2^{31}$ + pub const MIN: Self = Self(H); + /// The largest possible value of hardened index. Equals to $2^{32} - 1$ + pub const MAX: Self = Self(u32::MAX); +} +impl NonHardenedIndex { + /// The smallest possible value of non-hardened index. Equals to $0$ + pub const MIN: Self = Self(0); + /// The largest possible value of non-hardened index. Equals to $2^{31} - 1$ + pub const MAX: Self = Self(H - 1); +} +impl ops::Deref for HardenedIndex { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl ops::Deref for NonHardenedIndex { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl ops::Deref for ChildIndex { + type Target = u32; + fn deref(&self) -> &Self::Target { + match self { + Self::Hardened(i) => i, + Self::NonHardened(i) => i, + } + } +} +impl From for ChildIndex { + fn from(value: u32) -> Self { + match value { + H.. => Self::Hardened(HardenedIndex(value)), + _ => Self::NonHardened(NonHardenedIndex(value)), + } + } +} +impl TryFrom for HardenedIndex { + type Error = errors::OutOfRange; + fn try_from(value: u32) -> Result { + match ChildIndex::from(value) { + ChildIndex::Hardened(v) => Ok(v), + _ => Err(errors::OutOfRange), + } + } +} +impl TryFrom for NonHardenedIndex { + type Error = errors::OutOfRange; + fn try_from(value: u32) -> Result { + match ChildIndex::from(value) { + ChildIndex::NonHardened(v) => Ok(v), + _ => Err(errors::OutOfRange), + } + } +} +impl core::str::FromStr for ChildIndex { + type Err = core::num::ParseIntError; + fn from_str(s: &str) -> Result { + s.parse::().map(Into::into) + } +} +impl core::str::FromStr for HardenedIndex { + type Err = errors::ParseChildIndexError; + fn from_str(s: &str) -> Result { + let index = s + .parse::() + .map_err(errors::ParseChildIndexError::ParseInt)?; + HardenedIndex::try_from(index).map_err(errors::ParseChildIndexError::IndexNotInRange) + } +} +impl core::str::FromStr for NonHardenedIndex { + type Err = errors::ParseChildIndexError; + fn from_str(s: &str) -> Result { + let index = s + .parse::() + .map_err(errors::ParseChildIndexError::ParseInt)?; + NonHardenedIndex::try_from(index).map_err(errors::ParseChildIndexError::IndexNotInRange) + } +} + +impl From<&ExtendedSecretKey> for ExtendedPublicKey { + fn from(sk: &ExtendedSecretKey) -> Self { + ExtendedPublicKey { + public_key: Point::generator() * &sk.secret_key, + chain_code: sk.chain_code, + } + } +} + +impl From> for ExtendedKeyPair { + fn from(secret_key: ExtendedSecretKey) -> Self { + Self { + public_key: (&secret_key).into(), + secret_key, + } + } +} + +impl ExtendedKeyPair { + /// Returns chain code of the key + pub fn chain_code(&self) -> &ChainCode { + debug_assert_eq!(self.public_key.chain_code, self.secret_key.chain_code); + &self.public_key.chain_code + } + + /// Returns extended public key + pub fn public_key(&self) -> &ExtendedPublicKey { + &self.public_key + } + + /// Returns extended secret key + pub fn secret_key(&self) -> &ExtendedSecretKey { + &self.secret_key + } +} + +/// Marker for a curve supported by SLIP10 specs and this library +/// +/// Only implement this trait for the curves that are supported by SLIP10 specs. +/// Curves provided by the crate out-of-box in [supported_curves] module already +/// implement this trait. +pub trait SupportedCurve { + /// Specifies which curve it is + const CURVE_TYPE: CurveType; +} +#[cfg(feature = "curve-secp256k1")] +impl SupportedCurve for supported_curves::Secp256k1 { + const CURVE_TYPE: CurveType = CurveType::Secp256k1; +} +#[cfg(feature = "curve-secp256r1")] +impl SupportedCurve for supported_curves::Secp256r1 { + const CURVE_TYPE: CurveType = CurveType::Secp256r1; +} + +/// Curves supported by SLIP-10 spec +/// +/// It's either secp256k1 or secp256r1. Note that SLIP-10 also supports ed25519 curve, but this library +/// does not support it. +/// +/// `CurveType` is only needed for master key derivation. +#[derive(Clone, Copy, Debug)] +pub enum CurveType { + /// Secp256k1 curve + Secp256k1, + /// Secp256r1 curve + Secp256r1, +} + +/// Derives a master key from the seed +/// +/// Seed must be 16-64 bytes long, otherwise an error is returned +pub fn derive_master_key( + seed: &[u8], +) -> Result, errors::InvalidLength> { + let curve_tag = match E::CURVE_TYPE { + CurveType::Secp256k1 => "Bitcoin seed", + CurveType::Secp256r1 => "Nist256p1 seed", + }; + derive_master_key_with_curve_tag(curve_tag.as_bytes(), seed) +} + +/// Derives a master key from the seed and the curve tag as defined in SLIP10 +/// +/// It's preferred to use [derive_master_key] instead, as it automatically infers +/// the curve tag for supported curves. The curve tag is not validated by the function, +/// it's caller's responsibility to make sure that it complies with SLIP10. +/// +/// Seed must be 16-64 bytes long, otherwise an error is returned +pub fn derive_master_key_with_curve_tag( + curve_tag: &[u8], + seed: &[u8], +) -> Result, errors::InvalidLength> { + if !(16 <= seed.len() && seed.len() <= 64) { + return Err(errors::InvalidLength); + } + + 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) = split_into_two_halfes(&i); + + if let Ok(mut sk) = Scalar::::from_be_bytes(i_left) { + if !bool::from(subtle::ConstantTimeEq::ct_eq(&sk, &Scalar::zero())) { + return Ok(ExtendedSecretKey { + secret_key: SecretScalar::new(&mut sk), + chain_code: (*i_right).into(), + }); + } + } + + i = hmac.clone().chain_update(&i[..]).finalize().into_bytes() + } +} + +/// Derives child key pair (extended secret key + public key) from parent key pair +/// +/// ### Example +/// Derive child key m/1H from master key +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// +/// # let seed = b"do not use this seed :)".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); +/// +/// let derived_key = slip_10::derive_child_key_pair( +/// &master_key_pair, +/// 1 + slip_10::H, +/// ); +/// # Ok::<(), Box>(()) +/// ``` +pub fn derive_child_key_pair( + parent_key: &ExtendedKeyPair, + child_index: impl Into, +) -> ExtendedKeyPair { + let child_index = child_index.into(); + let shift = match child_index { + ChildIndex::Hardened(i) => derive_hardened_shift(parent_key, i), + ChildIndex::NonHardened(i) => derive_public_shift(&parent_key.public_key, i), + }; + let mut child_sk = &parent_key.secret_key.secret_key + shift.shift; + let child_sk = SecretScalar::new(&mut child_sk); + ExtendedKeyPair { + secret_key: ExtendedSecretKey { + secret_key: child_sk, + chain_code: shift.child_public_key.chain_code, + }, + public_key: shift.child_public_key, + } +} + +/// Derives a child key pair with specified derivation path from parent key pair +/// +/// Derivation path is an iterator that yields child indexes. +/// +/// If derivation path is empty, `parent_key` is returned +/// +/// ### Example +/// Derive a child key with path m/1/10/1H +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// # let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); +/// +/// let child_key = slip_10::derive_child_key_pair_with_path( +/// &master_key_pair, +/// [1, 10, 1 + slip_10::H], +/// ); +/// # Ok::<(), Box>(()) +/// ``` +pub fn derive_child_key_pair_with_path( + parent_key: &ExtendedKeyPair, + path: impl IntoIterator>, +) -> ExtendedKeyPair { + let result = try_derive_child_key_pair_with_path( + parent_key, + path.into_iter().map(Ok::<_, core::convert::Infallible>), + ); + match result { + Ok(key) => key, + Err(err) => match err {}, + } +} + +/// Derives a child key pair with specified derivation path from parent key pair +/// +/// Derivation path is a fallible iterator that yields child indexes. If iterator +/// yields an error, it's propagated to the caller. +/// +/// ### Example +/// Parse a path from the string and derive a child without extra allocations: +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// # let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); +/// +/// let path = "1/10/2"; +/// let child_indexes = path.split('/').map(str::parse::); +/// let child_key = slip_10::try_derive_child_key_pair_with_path( +/// &master_key_pair, +/// child_indexes, +/// )?; +/// # Ok::<_, Box>(()) +/// ``` +pub fn try_derive_child_key_pair_with_path( + parent_key: &ExtendedKeyPair, + path: impl IntoIterator, Err>>, +) -> Result, Err> { + let mut derived_key = parent_key.clone(); + for child_index in path { + derived_key = derive_child_key_pair(&derived_key, child_index?); + } + Ok(derived_key) +} + +/// Derives child extended public key from parent extended public key +/// +/// ### Example +/// Derive a master public key m/1 +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// +/// # let seed = b"do not use this seed :)".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_public_key = slip_10::ExtendedPublicKey::from(&master_key); +/// +/// let derived_key = slip_10::derive_child_public_key( +/// &master_public_key, +/// 1.try_into()?, +/// ); +/// # Ok::<(), Box>(()) +/// ``` +pub fn derive_child_public_key( + parent_public_key: &ExtendedPublicKey, + child_index: NonHardenedIndex, +) -> ExtendedPublicKey { + derive_public_shift(parent_public_key, child_index).child_public_key +} + +/// Derives a child public key with specified derivation path +/// +/// Derivation path is an iterator that yields child indexes. +/// +/// If derivation path is empty, `parent_public_key` is returned +/// +/// ### Example +/// Derive a child key with path m/1/10 +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// # let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_public_key = slip_10::ExtendedPublicKey::from(&master_key); +/// +/// let child_key = slip_10::derive_child_public_key_with_path( +/// &master_public_key, +/// [1.try_into()?, 10.try_into()?], +/// ); +/// # Ok::<(), Box>(()) +/// ``` +pub fn derive_child_public_key_with_path( + parent_public_key: &ExtendedPublicKey, + path: impl IntoIterator, +) -> ExtendedPublicKey { + let result = try_derive_child_public_key_with_path( + parent_public_key, + path.into_iter().map(Ok::<_, core::convert::Infallible>), + ); + match result { + Ok(key) => key, + Err(err) => match err {}, + } +} + +/// Derives a child public key with specified derivation path +/// +/// Derivation path is a fallible iterator that yields child indexes. If iterator +/// yields an error, it's propagated to the caller. +/// +/// ### Example +/// Parse a path from the string and derive a child without extra allocations: +/// ```rust +/// use slip_10::supported_curves::Secp256k1; +/// # let seed = b"16-64 bytes of high entropy".as_slice(); +/// let master_key = slip_10::derive_master_key::(seed)?; +/// let master_public_key = slip_10::ExtendedPublicKey::from(&master_key); +/// +/// let path = "1/10/2"; +/// let child_indexes = path.split('/').map(str::parse); +/// let child_key = slip_10::try_derive_child_public_key_with_path( +/// &master_public_key, +/// child_indexes, +/// )?; +/// # Ok::<_, Box>(()) +/// ``` +pub fn try_derive_child_public_key_with_path( + parent_public_key: &ExtendedPublicKey, + path: impl IntoIterator>, +) -> Result, Err> { + let mut derived_key = *parent_public_key; + for child_index in path { + derived_key = derive_child_public_key(&derived_key, child_index?); + } + Ok(derived_key) +} + +/// Derive a shift for hardened child +pub 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(); + calculate_shift(&hmac, &parent_key.public_key, *child_index, i) +} + +/// Derives a shift for non-hardened child +pub 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(); + calculate_shift(&hmac, parent_public_key, *child_index, i) +} + +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_halfes(&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_halfes( + i: &GenericArray, +) -> (&GenericArray, &GenericArray) { + generic_array::sequence::Split::split(i) +} diff --git a/tests/test_vectors.rs b/tests/test_vectors.rs new file mode 100644 index 0000000..d17fcd4 --- /dev/null +++ b/tests/test_vectors.rs @@ -0,0 +1,357 @@ +use generic_ec::Curve; +use hex_literal::hex; + +struct TestVector { + curve_type: slip_10::CurveType, + seed: &'static [u8], + derivations: &'static [Derivation], +} + +struct Derivation { + path: &'static [u32], + + expected_chain_code: slip_10::ChainCode, + expected_secret_key: [u8; 32], + expected_public_key: [u8; 33], +} + +/// Test vectors defined in +/// https://github.com/satoshilabs/slips/blob/817d54acc9989793288910a40f9eb59bebef3c6e/slip-0010.md#test-vectors +const TEST_VECTORS: &[TestVector] = &[ + // Test vector 1 for secp256k1 + TestVector { + seed: &hex!("000102030405060708090a0b0c0d0e0f"), + curve_type: slip_10::CurveType::Secp256k1, + derivations: &[ + Derivation { + path: &[], + expected_chain_code: hex!( + "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" + ), + expected_secret_key: hex!( + "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + ), + expected_public_key: hex!( + "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" + ), + }, + Derivation { + path: &[0 + slip_10::H], + expected_chain_code: hex!( + "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141" + ), + expected_secret_key: hex!( + "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea" + ), + expected_public_key: hex!( + "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1], + expected_chain_code: hex!( + "2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19" + ), + expected_secret_key: hex!( + "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368" + ), + expected_public_key: hex!( + "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H], + expected_chain_code: hex!( + "04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f" + ), + expected_secret_key: hex!( + "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca" + ), + expected_public_key: hex!( + "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H, 2], + expected_chain_code: hex!( + "cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd" + ), + expected_secret_key: hex!( + "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4" + ), + expected_public_key: hex!( + "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H, 2, 1000000000], + expected_chain_code: hex!( + "c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e" + ), + expected_secret_key: hex!( + "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8" + ), + expected_public_key: hex!( + "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011" + ), + }, + ], + }, + // Test vector 1 for nist256p1 + TestVector { + curve_type: slip_10::CurveType::Secp256r1, + seed: &hex!("000102030405060708090a0b0c0d0e0f"), + derivations: &[ + Derivation { + path: &[], + expected_chain_code: hex!( + "beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea" + ), + expected_secret_key: hex!( + "612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2" + ), + expected_public_key: hex!( + "0266874dc6ade47b3ecd096745ca09bcd29638dd52c2c12117b11ed3e458cfa9e8" + ), + }, + Derivation { + path: &[0 + slip_10::H], + expected_chain_code: hex!( + "3460cea53e6a6bb5fb391eeef3237ffd8724bf0a40e94943c98b83825342ee11" + ), + expected_secret_key: hex!( + "6939694369114c67917a182c59ddb8cafc3004e63ca5d3b84403ba8613debc0c" + ), + expected_public_key: hex!( + "0384610f5ecffe8fda089363a41f56a5c7ffc1d81b59a612d0d649b2d22355590c" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1], + expected_chain_code: hex!( + "4187afff1aafa8445010097fb99d23aee9f599450c7bd140b6826ac22ba21d0c" + ), + expected_secret_key: hex!( + "284e9d38d07d21e4e281b645089a94f4cf5a5a81369acf151a1c3a57f18b2129" + ), + expected_public_key: hex!( + "03526c63f8d0b4bbbf9c80df553fe66742df4676b241dabefdef67733e070f6844" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H], + expected_chain_code: hex!( + "98c7514f562e64e74170cc3cf304ee1ce54d6b6da4f880f313e8204c2a185318" + ), + expected_secret_key: hex!( + "694596e8a54f252c960eb771a3c41e7e32496d03b954aeb90f61635b8e092aa7" + ), + expected_public_key: hex!( + "0359cf160040778a4b14c5f4d7b76e327ccc8c4a6086dd9451b7482b5a4972dda0" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H, 2], + expected_chain_code: hex!( + "ba96f776a5c3907d7fd48bde5620ee374d4acfd540378476019eab70790c63a0" + ), + expected_secret_key: hex!( + "5996c37fd3dd2679039b23ed6f70b506c6b56b3cb5e424681fb0fa64caf82aaa" + ), + expected_public_key: hex!( + "029f871f4cb9e1c97f9f4de9ccd0d4a2f2a171110c61178f84430062230833ff20" + ), + }, + Derivation { + path: &[0 + slip_10::H, 1, 2 + slip_10::H, 2, 1000000000], + expected_chain_code: hex!( + "b9b7b82d326bb9cb5b5b121066feea4eb93d5241103c9e7a18aad40f1dde8059" + ), + expected_secret_key: hex!( + "21c4f269ef0a5fd1badf47eeacebeeaa3de22eb8e5b0adcd0f27dd99d34d0119" + ), + expected_public_key: hex!( + "02216cd26d31147f72427a453c443ed2cde8a1e53c9cc44e5ddf739725413fe3f4" + ), + }, + ], + }, + // Test vector 2 for secp256k1 + TestVector { + curve_type: slip_10::CurveType::Secp256k1, + seed: &hex!( + "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a2 + 9f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" + ), + derivations: &[ + Derivation { + path: &[], + expected_chain_code: hex!( + "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689" + ), + expected_secret_key: hex!( + "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e" + ), + expected_public_key: hex!( + "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7" + ), + }, + Derivation { + path: &[0], + expected_chain_code: hex!( + "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c" + ), + expected_secret_key: hex!( + "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e" + ), + expected_public_key: hex!( + "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea" + ), + }, + Derivation { + path: &[0, 2147483647 + slip_10::H], + expected_chain_code: hex!( + "be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9" + ), + expected_secret_key: hex!( + "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93" + ), + expected_public_key: hex!( + "03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b" + ), + }, + Derivation { + path: &[0, 2147483647 + slip_10::H, 1], + expected_chain_code: hex!( + "f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb" + ), + expected_secret_key: hex!( + "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7" + ), + expected_public_key: hex!( + "03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9" + ), + }, + Derivation { + path: &[0, 2147483647 + slip_10::H, 1, 2147483646 + slip_10::H], + expected_chain_code: hex!( + "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29" + ), + expected_secret_key: hex!( + "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d" + ), + expected_public_key: hex!( + "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0" + ), + }, + Derivation { + path: &[0, 2147483647 + slip_10::H, 1, 2147483646 + slip_10::H, 2], + expected_chain_code: hex!( + "9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271" + ), + expected_secret_key: hex!( + "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23" + ), + expected_public_key: hex!( + "024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c" + ), + }, + ], + }, + // Test derivation retry for nist256p1 + TestVector { + curve_type: slip_10::CurveType::Secp256r1, + seed: &hex!("000102030405060708090a0b0c0d0e0f"), + derivations: &[ + Derivation { + path: &[], + expected_chain_code: hex!( + "beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea" + ), + expected_secret_key: hex!( + "612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2" + ), + expected_public_key: hex!( + "0266874dc6ade47b3ecd096745ca09bcd29638dd52c2c12117b11ed3e458cfa9e8" + ), + }, + Derivation { + path: &[28578 + slip_10::H], + expected_chain_code: hex!( + "e94c8ebe30c2250a14713212f6449b20f3329105ea15b652ca5bdfc68f6c65c2" + ), + expected_secret_key: hex!( + "06f0db126f023755d0b8d86d4591718a5210dd8d024e3e14b6159d63f53aa669" + ), + expected_public_key: hex!( + "02519b5554a4872e8c9c1c847115363051ec43e93400e030ba3c36b52a3e70a5b7" + ), + }, + Derivation { + path: &[28578 + slip_10::H, 33941], + expected_chain_code: hex!( + "9e87fe95031f14736774cd82f25fd885065cb7c358c1edf813c72af535e83071" + ), + expected_secret_key: hex!( + "092154eed4af83e078ff9b84322015aefe5769e31270f62c3f66c33888335f3a" + ), + expected_public_key: hex!( + "0235bfee614c0d5b2cae260000bb1d0d84b270099ad790022c1ae0b2e782efe120" + ), + }, + ], + }, + // Test seed retry for nist256p1 + TestVector { + curve_type: slip_10::CurveType::Secp256r1, + seed: &hex!("a7305bc8df8d0951f0cb224c0e95d7707cbdf2c6ce7e8d481fec69c7ff5e9446"), + derivations: &[Derivation { + path: &[], + expected_chain_code: hex!( + "7762f9729fed06121fd13f326884c82f59aa95c57ac492ce8c9654e60efd130c" + ), + expected_secret_key: hex!( + "3b8c18469a4634517d6d0b65448f8e6c62091b45540a1743c5846be55d47d88f" + ), + expected_public_key: hex!( + "0383619fadcde31063d8c5cb00dbfe1713f3e6fa169d8541a798752a1c1ca0cb20" + ), + }], + }, +]; + +#[test] +fn test_vectors() { + for vector in TEST_VECTORS { + match vector.curve_type { + slip_10::CurveType::Secp256k1 => { + run_vector::(vector) + } + slip_10::CurveType::Secp256r1 => { + run_vector::(vector) + } + } + } +} + +fn run_vector(v: &TestVector) { + let master_key = slip_10::derive_master_key::(&v.seed).unwrap(); + let master_key_pair = slip_10::ExtendedKeyPair::from(master_key); + + for derivation in v.derivations { + let key = slip_10::derive_child_key_pair_with_path( + &master_key_pair, + derivation.path.iter().copied(), + ); + + assert_eq!(key.chain_code(), &derivation.expected_chain_code); + assert_eq!( + &key.public_key().public_key.to_bytes(true)[..], + &derivation.expected_public_key, + ); + assert_eq!( + &key.secret_key().secret_key.as_ref().to_be_bytes()[..], + &derivation.expected_secret_key, + ); + } +}