diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef4c795a9a..b7a3ee6b242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). -## Unreleased +## [Unreleased] + +### Added + +- Added support for new Clarity 4 builtin, `secp256r1-verify?` (not activated until epoch 3.3) ### Changed - Renamed Clarity 4's new `block-time` to `stacks-block-time` - Improve cost-tracking for type-checking function arguments in epoch 3.3 (see [#6425](https://github.com/stacks-network/stacks-core/issues/6425)) +- Replaced `libsecp256k1` with `k256` and `p256` from RustCrypto and removed separate Wasm implementations. ## [3.2.0.0.2] diff --git a/Cargo.lock b/Cargo.lock index 500e15aaf34..f0235d82fbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.12.3" @@ -735,6 +741,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[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 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -824,7 +842,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -854,6 +874,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -880,6 +915,26 @@ dependencies = [ "zeroize", ] +[[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 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -998,6 +1053,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.6" @@ -1172,6 +1237,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1216,6 +1282,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -1339,6 +1416,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac-drbg" version = "0.3.0" @@ -1347,7 +1433,7 @@ checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", "generic-array", - "hmac", + "hmac 0.8.1", ] [[package]] @@ -1633,6 +1719,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2 0.10.8", +] + [[package]] name = "keccak" version = "0.1.5" @@ -1714,7 +1814,6 @@ dependencies = [ "base64 0.22.1", "digest 0.9.0", "hmac-drbg", - "lazy_static", "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", @@ -1811,7 +1910,6 @@ name = "libstackerdb" version = "0.0.1" dependencies = [ "clarity 0.0.1", - "secp256k1", "serde", "sha2 0.10.8", "stacks-common 0.0.1", @@ -2108,6 +2206,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2 0.10.8", +] + [[package]] name = "parking" version = "2.2.0" @@ -2283,6 +2394,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2580,6 +2701,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.7" @@ -2825,6 +2956,21 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" +[[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", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.24.3" @@ -2929,6 +3075,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -3039,6 +3195,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3175,14 +3332,14 @@ dependencies = [ "ed25519-dalek", "getrandom 0.2.12", "hashbrown 0.15.2", + "k256", "lazy_static", - "libsecp256k1", "nix", + "p256", "proptest", "rand 0.8.5", "ripemd", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", @@ -3191,6 +3348,7 @@ dependencies = [ "slog", "slog-json", "slog-term", + "thiserror", "toml", "winapi 0.3.9", ] @@ -3307,7 +3465,6 @@ dependencies = [ "rand_core 0.6.4", "reqwest", "rusqlite", - "secp256k1", "serde", "serde_json", "slog", @@ -3381,7 +3538,6 @@ dependencies = [ "rstest", "rstest_reuse", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", diff --git a/clarity/fuzz/Cargo.lock b/clarity/fuzz/Cargo.lock index c8c5e852896..aaef48b3751 100644 --- a/clarity/fuzz/Cargo.lock +++ b/clarity/fuzz/Cargo.lock @@ -50,12 +50,6 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "autocfg" version = "1.5.0" @@ -63,10 +57,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base64" -version = "0.22.1" +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" @@ -80,15 +80,6 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -175,6 +166,12 @@ dependencies = [ "stacks-common", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -191,10 +188,16 @@ dependencies = [ ] [[package]] -name = "crunchy" -version = "0.2.4" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" @@ -206,16 +209,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -225,7 +218,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", "rustc_version", "serde", @@ -244,21 +237,22 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.4.0" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "powerfmt", + "const-oid", + "zeroize", ] [[package]] -name = "digest" -version = "0.9.0" +name = "deranged" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ - "generic-array", + "powerfmt", ] [[package]] @@ -267,8 +261,10 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -292,6 +288,21 @@ dependencies = [ "winapi", ] +[[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", + "serdect", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -309,8 +320,28 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "sha2 0.10.9", + "sha2", + "subtle", +] + +[[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", + "pkcs8", + "rand_core", + "sec1", + "serdect", "subtle", + "zeroize", ] [[package]] @@ -331,6 +362,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -446,6 +487,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -471,6 +513,17 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[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 = "hashbrown" version = "0.14.5" @@ -509,23 +562,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - -[[package]] -name = "hmac-drbg" -version = "0.3.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.9.0", - "generic-array", - "hmac", + "digest", ] [[package]] @@ -598,6 +639,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", +] + [[package]] name = "keccak" version = "0.1.5" @@ -639,55 +694,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libsecp256k1" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" -dependencies = [ - "arrayref", - "base64", - "digest 0.9.0", - "hmac-drbg", - "lazy_static", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand", - "serde", - "sha2 0.9.9", - "typenum", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "libsqlite3-sys" version = "0.28.0" @@ -755,10 +761,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2", +] [[package]] name = "pin-project-lite" @@ -772,6 +785,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -793,6 +816,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + [[package]] name = "proc-macro2" version = "1.0.97" @@ -896,13 +929,23 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ripemd" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -980,22 +1023,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "secp256k1" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" -dependencies = [ - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.6.1" +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "cc", + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", ] [[package]] @@ -1047,16 +1086,13 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.9.9" +name = "serdect" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "base16ct", + "serde", ] [[package]] @@ -1067,7 +1103,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", "sha2-asm", ] @@ -1086,7 +1122,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -1101,6 +1137,10 @@ name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] [[package]] name = "slab" @@ -1133,6 +1173,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stacker" version = "0.1.21" @@ -1154,20 +1204,21 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "hashbrown 0.15.5", + "k256", "lazy_static", - "libsecp256k1", "nix", + "p256", "rand", "ripemd", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", - "sha2 0.10.9", + "sha2", "sha3", "slog", "slog-term", + "thiserror", "toml", "winapi", ] @@ -1559,3 +1610,9 @@ dependencies = [ "quote", "syn 2.0.104", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index fac859bace5..0d12aed24a6 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -207,8 +207,8 @@ impl ArithmeticOnlyChecker<'_> { IntToAscii | IntToUtf8 | StringToInt | StringToUInt | ToAscii => { Err(Error::FunctionNotPermitted(function)) } - Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Hash160 | Sha256 - | Keccak256 => Err(Error::FunctionNotPermitted(function)), + Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Secp256r1Verify + | Hash160 | Sha256 | Keccak256 => Err(Error::FunctionNotPermitted(function)), Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater | Modulo | Power | Sqrti | Log2 | BitwiseXor | And | Or | Not | Equals | If | ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet | UnwrapErrRet | IsOkay diff --git a/clarity/src/vm/analysis/read_only_checker/mod.rs b/clarity/src/vm/analysis/read_only_checker/mod.rs index 80700cad66b..f3e243a27a5 100644 --- a/clarity/src/vm/analysis/read_only_checker/mod.rs +++ b/clarity/src/vm/analysis/read_only_checker/mod.rs @@ -307,6 +307,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify + | Secp256r1Verify | ConsSome | ConsOkay | ConsError diff --git a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs index 827678dba48..059434f3937 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs @@ -813,7 +813,8 @@ impl TypedNativeFunction { | AllowanceWithFt | AllowanceWithNft | AllowanceWithStacking - | AllowanceAll => { + | AllowanceAll + | Secp256r1Verify => { return Err(CheckErrors::Expects( "Clarity 2+ keywords should not show up in 2.05".into(), )); diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index fd769a22d38..20fc1ba6a49 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -726,6 +726,18 @@ fn check_secp256k1_verify( Ok(TypeSignature::BoolType) } +fn check_secp256r1_verify( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_argument_count(3, args)?; + checker.type_check_expects(&args[0], context, &TypeSignature::BUFFER_32)?; + checker.type_check_expects(&args[1], context, &TypeSignature::BUFFER_64)?; + checker.type_check_expects(&args[2], context, &TypeSignature::BUFFER_33)?; + Ok(TypeSignature::BoolType) +} + fn check_get_block_info( checker: &mut TypeChecker, args: &[SymbolicExpression], @@ -1220,6 +1232,7 @@ impl TypedNativeFunction { | AllowanceWithNft | AllowanceWithStacking | AllowanceAll => Special(SpecialNativeFunction(&post_conditions::check_allowance_err)), + Secp256r1Verify => Special(SpecialNativeFunction(&check_secp256r1_verify)), }; Ok(out) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs index bd30b6b30cf..46ac04c3eba 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs @@ -42,6 +42,17 @@ mod assets; pub mod contracts; mod post_conditions; +const SECP256_MESSAGE_HASH: &str = + "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +const SECP256K1_SIGNATURE: &str = + "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40"; +const SECP256K1_SIGNATURE_TOO_LONG: &str = + "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041"; +const SECP256K1_PUBLIC_KEY: &str = + "0xfffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0df"; +const SECP256R1_SIGNATURE: &str = + "0x000306090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366696c6f7275787b7e8184878a8d909396999c9fa2a5a8abaeb1b4b7babd"; + /// Backwards-compatibility shim for type_checker tests. Runs at latest Clarity version. pub fn mem_type_check(exp: &str) -> Result<(Option, ContractAnalysis), CheckError> { mem_run_analysis( @@ -3856,3 +3867,192 @@ fn test_nested_bad_type_signature_syntax_bindings() { assert_eq!(*expected_err, *type_check_helper(bad_code).unwrap_err().err); } } + +#[test] +fn test_secp256k1_recover_type_check() { + let good_expr = format!( + "(secp256k1-recover? {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE + ); + let type_result = type_check_helper(&good_expr).unwrap(); + assert_eq!("(response (buff 33) uint)", type_result.to_string()); + + let buffer_66_type = TypeSignature::SequenceType(BufferType( + BufferLength::try_from(66u32).expect("BufferLength::try_from failed"), + )); + + let bad_cases = [ + ( + "(secp256k1-recover?)".to_string(), + CheckErrors::IncorrectArgumentCount(2, 0), + ), + ( + format!( + "(secp256k1-recover? {} {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE, SECP256K1_PUBLIC_KEY + ), + CheckErrors::IncorrectArgumentCount(2, 3), + ), + ( + format!( + "(secp256k1-recover? {} {})", + SECP256K1_SIGNATURE, SECP256K1_SIGNATURE + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_32), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ( + format!( + "(secp256k1-recover? {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE_TOO_LONG + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_65), + Box::new(buffer_66_type.clone()), + ), + ), + ]; + + for (bad_expr, expected_err) in bad_cases.iter() { + println!("checking bad expr: {}", bad_expr); + let result = type_check_helper(bad_expr); + assert!( + result.is_err(), + "expression `{}` unexpectedly type-checked", + bad_expr + ); + assert_eq!(*expected_err, *result.unwrap_err().err); + } +} + +#[test] +fn test_secp256k1_verify_type_check() { + let good_expr = format!( + "(secp256k1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE, SECP256K1_PUBLIC_KEY + ); + assert_eq!("bool", type_check_helper(&good_expr).unwrap().to_string()); + + let buffer_66_type = TypeSignature::SequenceType(BufferType( + BufferLength::try_from(66u32).expect("BufferLength::try_from failed"), + )); + + let bad_cases = [ + ( + "(secp256k1-verify)".to_string(), + CheckErrors::IncorrectArgumentCount(3, 0), + ), + ( + format!( + "(secp256k1-verify {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE + ), + CheckErrors::IncorrectArgumentCount(3, 2), + ), + ( + format!( + "(secp256k1-verify {} {} {})", + SECP256K1_SIGNATURE, SECP256K1_SIGNATURE, SECP256K1_PUBLIC_KEY + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_32), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ( + format!( + "(secp256k1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE_TOO_LONG, SECP256K1_PUBLIC_KEY + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_65), + Box::new(buffer_66_type.clone()), + ), + ), + ( + format!( + "(secp256k1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE, SECP256K1_SIGNATURE + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_33), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ]; + + for (bad_expr, expected_err) in bad_cases.iter() { + let result = type_check_helper(bad_expr); + assert!( + result.is_err(), + "expression `{}` unexpectedly type-checked", + bad_expr + ); + assert_eq!(*expected_err, *result.unwrap_err().err); + } +} + +#[test] +fn test_secp256r1_verify_type_check() { + let good_expr = format!( + "(secp256r1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256R1_SIGNATURE, SECP256K1_PUBLIC_KEY + ); + assert_eq!("bool", type_check_helper(&good_expr).unwrap().to_string()); + + let bad_cases = [ + ( + "(secp256r1-verify)".to_string(), + CheckErrors::IncorrectArgumentCount(3, 0), + ), + ( + format!( + "(secp256r1-verify {} {})", + SECP256_MESSAGE_HASH, SECP256R1_SIGNATURE + ), + CheckErrors::IncorrectArgumentCount(3, 2), + ), + ( + format!( + "(secp256r1-verify {} {} {})", + SECP256K1_SIGNATURE, SECP256R1_SIGNATURE, SECP256K1_PUBLIC_KEY + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_32), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ( + format!( + "(secp256r1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256K1_SIGNATURE, SECP256K1_PUBLIC_KEY + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_64), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ( + format!( + "(secp256r1-verify {} {} {})", + SECP256_MESSAGE_HASH, SECP256R1_SIGNATURE, SECP256K1_SIGNATURE + ), + CheckErrors::TypeError( + Box::new(TypeSignature::BUFFER_33), + Box::new(TypeSignature::BUFFER_65), + ), + ), + ]; + + for (bad_expr, expected_err) in bad_cases.iter() { + let result = type_check_helper(bad_expr); + assert!( + result.is_err(), + "expression `{}` unexpectedly type-checked", + bad_expr + ); + assert_eq!(*expected_err, *result.unwrap_err().err); + } +} diff --git a/clarity/src/vm/costs/cost_functions.rs b/clarity/src/vm/costs/cost_functions.rs index 055232ff4ea..b51179e2ea8 100644 --- a/clarity/src/vm/costs/cost_functions.rs +++ b/clarity/src/vm/costs/cost_functions.rs @@ -161,6 +161,7 @@ define_named_enum!(ClarityCostFunction { ToAscii("cost_to_ascii"), RestrictAssets("cost_restrict_assets"), AsContractSafe("cost_as_contract_safe"), + Secp256r1verify("cost_secp256r1verify"), Unimplemented("cost_unimplemented"), }); @@ -334,6 +335,7 @@ pub trait CostValues { fn cost_to_ascii(n: u64) -> InterpreterResult; fn cost_restrict_assets(n: u64) -> InterpreterResult; fn cost_as_contract_safe(n: u64) -> InterpreterResult; + fn cost_secp256r1verify(n: u64) -> InterpreterResult; } impl ClarityCostFunction { @@ -490,6 +492,7 @@ impl ClarityCostFunction { ClarityCostFunction::ToAscii => C::cost_to_ascii(n), ClarityCostFunction::RestrictAssets => C::cost_restrict_assets(n), ClarityCostFunction::AsContractSafe => C::cost_as_contract_safe(n), + ClarityCostFunction::Secp256r1verify => C::cost_secp256r1verify(n), ClarityCostFunction::Unimplemented => Err(RuntimeErrorType::NotImplemented.into()), } } diff --git a/clarity/src/vm/costs/costs_1.rs b/clarity/src/vm/costs/costs_1.rs index 7afbe365a91..fa16282da84 100644 --- a/clarity/src/vm/costs/costs_1.rs +++ b/clarity/src/vm/costs/costs_1.rs @@ -761,4 +761,8 @@ impl CostValues for Costs1 { fn cost_as_contract_safe(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_secp256r1verify(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2.rs b/clarity/src/vm/costs/costs_2.rs index bdb4fa3e817..8c44f814063 100644 --- a/clarity/src/vm/costs/costs_2.rs +++ b/clarity/src/vm/costs/costs_2.rs @@ -761,4 +761,8 @@ impl CostValues for Costs2 { fn cost_as_contract_safe(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_secp256r1verify(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2_testnet.rs b/clarity/src/vm/costs/costs_2_testnet.rs index d942d830199..05baabd0b18 100644 --- a/clarity/src/vm/costs/costs_2_testnet.rs +++ b/clarity/src/vm/costs/costs_2_testnet.rs @@ -761,4 +761,8 @@ impl CostValues for Costs2Testnet { fn cost_as_contract_safe(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_secp256r1verify(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_3.rs b/clarity/src/vm/costs/costs_3.rs index 46ae6796ed4..8e34ad6b1de 100644 --- a/clarity/src/vm/costs/costs_3.rs +++ b/clarity/src/vm/costs/costs_3.rs @@ -779,4 +779,8 @@ impl CostValues for Costs3 { fn cost_as_contract_safe(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_secp256r1verify(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_4.rs b/clarity/src/vm/costs/costs_4.rs index aca4731eb27..f8c2ea26066 100644 --- a/clarity/src/vm/costs/costs_4.rs +++ b/clarity/src/vm/costs/costs_4.rs @@ -472,4 +472,9 @@ impl CostValues for Costs4 { // TODO: needs criterion benchmark Ok(ExecutionCost::runtime(linear(n, 1, 100))) } + + fn cost_secp256r1verify(n: u64) -> InterpreterResult { + // TODO: needs criterion benchmark + Ok(ExecutionCost::runtime(1)) + } } diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index c258bc199b1..378837b8818 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -1369,8 +1369,8 @@ const SECP256K1RECOVER_API: SpecialAPI = SpecialAPI { snippet: "secp256k1-recover? ${1:message-hash} ${2:signature}", output_type: "(response (buff 33) uint)", signature: "(secp256k1-recover? message-hash signature)", - description: "The `secp256k1-recover?` function recovers the public key used to sign the message whose sha256 is `message-hash` -with the provided `signature`. The signature includes 64 bytes plus an additional recovery id (00..03) for a total of 65 bytes. + description: "The `secp256k1-recover?` function recovers the public key used to sign the hash, `message-hash`, typically +a sha256 hash, with the provided `signature`. The signature includes 64 bytes plus an additional recovery id (00..03) for a total of 65 bytes. On success, it returns the public key as a 33-byte buffer. This function may fail with one of the following error codes: * `(err u1)` - the signature does not match the message hash @@ -1388,8 +1388,9 @@ const SECP256K1VERIFY_API: SpecialAPI = SpecialAPI { signature: "(secp256k1-verify message-hash signature public-key)", description: "The `secp256k1-verify` function verifies that the provided signature of the message-hash was signed with the private key that generated the public key. -The `message-hash` is the `sha256` of the message. -The signature includes 64 bytes plus an optional additional recovery id (00..03) for a total of 64 or 65 bytes.", +The `message-hash` is typically the `sha256` of a message. +The signature includes 64 bytes plus an optional additional recovery id (00..03) for a total of 64 or 65 bytes. +High-S signatures are rejected to enforce the canonical low-S form and prevent malleable signatures.", example: "(secp256k1-verify 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a1301 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110) ;; Returns true @@ -1401,6 +1402,24 @@ The signature includes 64 bytes plus an optional additional recovery id (00..03) 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110) ;; Returns false" }; +const SECP256R1VERIFY_API: SpecialAPI = SpecialAPI { + input_type: "(buff 32), (buff 64), (buff 33)", + snippet: "secp256r1-verify ${1:message-hash} ${2:signature} ${3:public-key})", + output_type: "bool", + signature: "(secp256r1-verify message-hash signature public-key)", + description: "The `secp256r1-verify` function verifies that the provided signature of the message-hash +was signed with the private key that generated the public key. +`message-hash` is typically the `sha256` of a message and `signature` is the raw 64-byte signature. +High-S signatures are allowed. +Note that this is NOT the Bitcoin (or default Stacks) signature scheme, secp256k1, but rather the +NIST P-256 curve (also known as secp256r1).", + example: "(secp256r1-verify 0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8 + 0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd310eac59293c23e6d6aa8b0c5d9e4e48fa4c4fdf1ace2ba618dc0263b5e90a0903 0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e) ;; Returns true +(secp256r1-verify 0x0000000000000000000000000000000000000000000000000000000000000000 + 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + 0x037a6b62e3c8b14f1b5933f5d5ab0509a8e7d95a111b8d3b264d95bfa753b00296) ;; Returns false" +}; + const CONTRACT_CALL_API: SpecialAPI = SpecialAPI { input_type: "ContractName, PublicFunctionName, Arg0, ...", snippet: "contract-call? ${1:contract-principal} ${2:func} ${3:arg1}", @@ -2881,6 +2900,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { AllowanceWithNft => make_for_special(&ALLOWANCE_WITH_NFT, function), AllowanceWithStacking => make_for_special(&ALLOWANCE_WITH_STACKING, function), AllowanceAll => make_for_special(&ALLOWANCE_WITH_ALL, function), + Secp256r1Verify => make_for_special(&SECP256R1VERIFY_API, function), } } diff --git a/clarity/src/vm/functions/crypto.rs b/clarity/src/vm/functions/crypto.rs index 9277860faec..9628c735e87 100644 --- a/clarity/src/vm/functions/crypto.rs +++ b/clarity/src/vm/functions/crypto.rs @@ -20,6 +20,7 @@ use stacks_common::address::{ use stacks_common::types::chainstate::StacksAddress; use stacks_common::util::hash; use stacks_common::util::secp256k1::{secp256k1_recover, secp256k1_verify, Secp256k1PublicKey}; +use stacks_common::util::secp256r1::secp256r1_verify; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; @@ -285,3 +286,94 @@ pub fn special_secp256k1_verify( secp256k1_verify(message, signature, pubkey).is_ok(), )) } + +pub fn special_secp256r1_verify( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> Result { + // (secp256r1-verify message-hash signature public-key) + // message-hash: (buff 32), signature: (buff 64), public-key: (buff 33) + check_argument_count(3, args)?; + + runtime_cost(ClarityCostFunction::Secp256r1verify, env, 0)?; + + let arg0 = args + .first() + .ok_or(CheckErrors::IncorrectArgumentCount(0, 3))?; + let message_value = eval(arg0, env, context)?; + let message = match message_value { + Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { + if data.len() != 32 { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_32), + Box::new(message_value), + ) + .into()); + } + data + } + _ => { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_32), + Box::new(message_value), + ) + .into()) + } + }; + + let arg1 = args + .get(1) + .ok_or(CheckErrors::IncorrectArgumentCount(1, 3))?; + let signature_value = eval(arg1, env, context)?; + let signature = match signature_value { + Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { + if data.len() > 64 { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_64), + Box::new(signature_value), + ) + .into()); + } + if data.len() != 64 { + return Ok(Value::Bool(false)); + } + data + } + _ => { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_64), + Box::new(signature_value), + ) + .into()) + } + }; + + let arg2 = args + .get(2) + .ok_or(CheckErrors::IncorrectArgumentCount(2, 3))?; + let pubkey_value = eval(arg2, env, context)?; + let pubkey = match pubkey_value { + Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { + if data.len() != 33 { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_33), + Box::new(pubkey_value), + ) + .into()); + } + data + } + _ => { + return Err(CheckErrors::TypeValueError( + Box::new(TypeSignature::BUFFER_33), + Box::new(pubkey_value), + ) + .into()) + } + }; + + Ok(Value::Bool( + secp256r1_verify(message, signature, pubkey).is_ok(), + )) +} diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index df58bb845c6..37aef1e0804 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -201,6 +201,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { AllowanceWithNft("with-nft", ClarityVersion::Clarity4, None), AllowanceWithStacking("with-stacking", ClarityVersion::Clarity4, None), AllowanceAll("with-all-assets-unsafe", ClarityVersion::Clarity4, None), + Secp256r1Verify("secp256r1-verify", ClarityVersion::Clarity4, None), }); /// @@ -587,6 +588,9 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option | AllowanceAll => { SpecialFunction("special_allowance", &post_conditions::special_allowance) } + Secp256r1Verify => { + SpecialFunction("native_secp256r1-verify", &crypto::special_secp256r1_verify) + } }; Some(callable) } else { diff --git a/clarity/src/vm/tests/crypto.rs b/clarity/src/vm/tests/crypto.rs new file mode 100644 index 00000000000..5565e90adf1 --- /dev/null +++ b/clarity/src/vm/tests/crypto.rs @@ -0,0 +1,372 @@ +use stacks_common::types::chainstate::{StacksPrivateKey, StacksPublicKey}; +use stacks_common::types::{PrivateKey, StacksEpochId}; +use stacks_common::util::hash::{to_hex, Sha256Sum}; +use stacks_common::util::secp256k1::MessageSignature as Secp256k1Signature; +use stacks_common::util::secp256r1::{Secp256r1PrivateKey, Secp256r1PublicKey}; + +use crate::vm::errors::{CheckErrors, Error}; +use crate::vm::types::{ResponseData, TypeSignature, Value}; +use crate::vm::{execute_with_parameters, ClarityVersion}; + +fn secp256r1_vectors() -> (Vec, Vec, Vec) { + let privk = Secp256r1PrivateKey::from_seed(&[7u8; 32]); + let pubk = Secp256r1PublicKey::from_private(&privk); + let message_hash = Sha256Sum::from_data(b"clarity-secp256r1-tests"); + let signature = privk + .sign(message_hash.as_bytes()) + .expect("secp256r1 signing should succeed"); + + ( + message_hash.as_bytes().to_vec(), + signature.0.to_vec(), + pubk.to_bytes_compressed(), + ) +} + +fn secp256k1_vectors() -> (Vec, Vec, Vec) { + let privk = StacksPrivateKey::from_seed(&[9u8; 32]); + let pubk = StacksPublicKey::from_private(&privk); + let message_hash = Sha256Sum::from_data(b"clarity-secp256k1-tests"); + let signature: Secp256k1Signature = privk + .sign(message_hash.as_bytes()) + .expect("secp256k1 signing should succeed"); + // Clarity expects R || S || v ordering. + let signature_bytes = signature.to_rsv(); + + ( + message_hash.as_bytes().to_vec(), + signature_bytes, + pubk.to_bytes_compressed(), + ) +} + +fn buff_literal(bytes: &[u8]) -> String { + format!("0x{}", to_hex(bytes)) +} + +fn zeroed_buff_literal(len: usize) -> String { + buff_literal(&vec![0u8; len]) +} + +#[test] +fn test_secp256r1_verify_valid_signature_returns_true() { + let (message, signature, pubkey) = secp256r1_vectors(); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256r1_verify_valid_high_s_signature_returns_true() { + let message = "0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8"; + let signature = "0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd31f153a6d5c3dc192a5574f3a261b1b70570971b92d8ebf86c17b7670d13591c4e"; + let pubkey = "0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e"; + + let program = format!("(secp256r1-verify {message} {signature} {pubkey})"); + + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256r1_verify_invalid_signature_returns_false() { + let (message, mut signature, pubkey) = secp256r1_vectors(); + signature[0] ^= 0x01; + + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256r1_verify_signature_too_short_returns_false() { + let (message, mut signature, pubkey) = secp256r1_vectors(); + signature.truncate(63); + + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256r1_verify_signature_too_long_errors() { + let (message, mut signature, pubkey) = secp256r1_vectors(); + signature.push(0x00); + + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + let err = execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false, + ) + .unwrap_err(); + match err { + Error::Unchecked(CheckErrors::TypeValueError(expected, _)) => { + assert_eq!(*expected, TypeSignature::BUFFER_64); + } + _ => panic!("expected BUFFER_65 type error, found {err:?}"), + } +} + +#[test] +fn test_secp256k1_verify_valid_signature_returns_true() { + let (message, signature, pubkey) = secp256k1_vectors(); + let program = format!( + "(secp256k1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + println!("program: {program}"); + + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_verify_valid_high_s_signature_returns_false() { + let message = "0x89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89"; + let signature = "0x54cd3f378a424a3e50ff1c911b7d80cf424e1b86dddecadbcf39077e62fa1e54ee6514347c1608df2c3995e7356f2d60a1fab60878214642134d78cd923ce27a01"; + let pubkey = "0x0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967"; + + let program = format!("(secp256k1-verify {message} {signature} {pubkey})"); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_verify_invalid_signature_returns_false() { + let (message, mut signature, pubkey) = secp256k1_vectors(); + signature[10] ^= 0x01; + + let program = format!( + "(secp256k1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_verify_signature_too_short_returns_false() { + let (message, mut signature, pubkey) = secp256k1_vectors(); + signature.truncate(63); + + let program = format!( + "(secp256k1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_verify_recovery_id_out_of_range_returns_false() { + let (message, mut signature, pubkey) = secp256k1_vectors(); + if let Some(last) = signature.last_mut() { + *last = 0x04; + } + + let program = format!( + "(secp256k1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_verify_signature_too_long_errors() { + let (message, mut signature, pubkey) = secp256k1_vectors(); + signature.extend([0x00, 0x01]); + + let program = format!( + "(secp256k1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + + let err = execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false, + ) + .unwrap_err(); + match err { + Error::Unchecked(CheckErrors::TypeValueError(expected, _)) => { + assert_eq!(*expected, TypeSignature::BUFFER_65); + } + _ => panic!("expected BUFFER_65 type error, found {err:?}"), + } +} + +#[test] +fn test_secp256k1_recover_returns_expected_public_key() { + let (message, signature, pubkey) = secp256k1_vectors(); + let fallback = zeroed_buff_literal(33); + let program = format!( + "(is-eq (unwrap! (secp256k1-recover? {} {}) {}) {})", + buff_literal(&message), + buff_literal(&signature), + fallback, + buff_literal(&pubkey) + ); + + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + +#[test] +fn test_secp256k1_recover_invalid_signature_returns_err_code() { + let (message, mut signature, _pubkey) = secp256k1_vectors(); + signature[5] ^= 0x02; + + let program = format!( + "(secp256k1-recover? {} {})", + buff_literal(&message), + buff_literal(&signature) + ); + + match execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false, + ) + .expect("execution should succeed") + .expect("should return a value") + { + Value::Response(ResponseData { data, .. }) => { + assert_eq!(data, Box::new(Value::UInt(1))); + } + other => panic!("expected err response, found {other:?}"), + } +} diff --git a/clarity/src/vm/tests/mod.rs b/clarity/src/vm/tests/mod.rs index 91c05eb9366..8b4d3dd2095 100644 --- a/clarity/src/vm/tests/mod.rs +++ b/clarity/src/vm/tests/mod.rs @@ -28,6 +28,8 @@ mod assets; mod contracts; #[cfg(test)] mod conversions; +#[cfg(test)] +mod crypto; mod datamaps; mod defines; #[cfg(test)] diff --git a/libstackerdb/Cargo.toml b/libstackerdb/Cargo.toml index c4910a7d25f..3594bdf71cc 100644 --- a/libstackerdb/Cargo.toml +++ b/libstackerdb/Cargo.toml @@ -20,10 +20,6 @@ serde = "1" stacks-common = { path = "../stacks-common" } clarity = { path = "../clarity" } -[dependencies.secp256k1] -version = "0.24.3" -features = ["serde", "recovery"] - [target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(any(target_os="windows"))))'.dependencies] sha2 = { version = "0.10", features = ["asm"] } diff --git a/stacks-common/Cargo.toml b/stacks-common/Cargo.toml index 8adaa41eeed..00f808c5eec 100644 --- a/stacks-common/Cargo.toml +++ b/stacks-common/Cargo.toml @@ -42,6 +42,11 @@ serde_json = { workspace = true } sha3 = { version = "0.10.1", default-features = false } slog = { workspace = true } slog-term = { version = "2.6.0", default-features = false } +thiserror = { workspace = true } + +# RustCrypto elliptic curve crates +k256 = { version = "0.13", default-features = false, features = ["std", "serde", "ecdsa"] } +p256 = { version = "0.13", default-features = false, features = ["std", "serde", "ecdsa"] } # Optional dependencies getrandom = { version = "0.2", default-features = false, optional = true } @@ -60,12 +65,8 @@ winapi = { version = "0.3", features = [ ], optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] -secp256k1 = { version = "0.24.3", default-features = false, features = ["std","serde", "recovery"] } rusqlite = { workspace = true, optional = true } -[target.'cfg(target_family = "wasm")'.dependencies] -libsecp256k1 = { version = "0.7.2", default-features = false, features = ["hmac", "lazy-static-context"] } - [target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(any(target_os="windows"))))'.dependencies] sha2 = { version = "0.10", features = ["asm"] } @@ -98,5 +99,5 @@ bech32_std = [] bech32_strict = [] # Wasm-specific features for easier configuration -wasm-web = ["rand", "getrandom/js", "libsecp256k1/static-context"] +wasm-web = ["rand", "getrandom/js"] wasm-deterministic = ["getrandom/custom"] diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index 78f246ada33..a687b6e7b25 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -69,12 +69,6 @@ pub trait PublicKey: Clone + fmt::Debug + serde::Serialize + serde::de::Deserial pub trait PrivateKey: Clone + fmt::Debug + serde::Serialize + serde::de::DeserializeOwned { fn to_bytes(&self) -> Vec; fn sign(&self, data_hash: &[u8]) -> Result; - #[cfg(any(test, feature = "testing"))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result; } pub trait Address: Clone + fmt::Debug + fmt::Display { diff --git a/stacks-common/src/util/mod.rs b/stacks-common/src/util/mod.rs index 69e14a44735..25be66f1461 100644 --- a/stacks-common/src/util/mod.rs +++ b/stacks-common/src/util/mod.rs @@ -27,6 +27,7 @@ pub mod pair; pub mod pipe; pub mod retry; pub mod secp256k1; +pub mod secp256r1; pub mod serde_serializers; pub mod uint; pub mod vrf; diff --git a/stacks-common/src/util/secp256k1.rs b/stacks-common/src/util/secp256k1.rs new file mode 100644 index 00000000000..694d414b7ce --- /dev/null +++ b/stacks-common/src/util/secp256k1.rs @@ -0,0 +1,1015 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::hash::{Hash, Hasher}; + +use k256::ecdsa::signature::hazmat::PrehashVerifier; +use k256::ecdsa::{ + RecoveryId as K256RecoveryId, Signature as K256Signature, SigningKey as K256SigningKey, + VerifyingKey as K256VerifyingKey, +}; +use k256::elliptic_curve::generic_array::GenericArray; +use k256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use k256::{EncodedPoint, PublicKey as K256PublicKey, SecretKey as K256SecretKey}; +use thiserror::Error; + +use crate::types::{PrivateKey, PublicKey}; +use crate::util::hash::{hex_bytes, to_hex, Sha256Sum}; + +pub const MESSAGE_SIGNATURE_ENCODED_SIZE: u32 = 65; + +pub struct MessageSignature(pub [u8; 65]); +impl_array_newtype!(MessageSignature, u8, 65); +impl_array_hexstring_fmt!(MessageSignature); +impl_byte_array_newtype!(MessageSignature, u8, 65); +impl_byte_array_serde!(MessageSignature); + +pub struct SchnorrSignature(pub [u8; 65]); +impl_array_newtype!(SchnorrSignature, u8, 65); +impl_array_hexstring_fmt!(SchnorrSignature); +impl_byte_array_newtype!(SchnorrSignature, u8, 65); +impl_byte_array_serde!(SchnorrSignature); +pub const SCHNORR_SIGNATURE_ENCODED_SIZE: u32 = 65; + +impl Default for SchnorrSignature { + /// Creates a default Schnorr Signature. Note this is not a valid signature. + fn default() -> Self { + Self([0u8; 65]) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Error)] +pub enum Secp256k1Error { + #[error("Invalid key")] + InvalidKey, + #[error("Invalid signature")] + InvalidSignature, + #[error("Invalid message")] + InvalidMessage, + #[error("Invalid recovery ID")] + InvalidRecoveryId, + #[error("Signing failed")] + SigningFailed, + #[error("Recovery failed")] + RecoveryFailed, +} + +/// An ECDSA recoverable signature, which includes the recovery ID. +pub struct RecoverableSignature { + signature: K256Signature, + recovery_id: K256RecoveryId, +} + +impl RecoverableSignature { + /// Converts a recoverable signature to a non-recoverable one. + pub fn to_standard(&self) -> SignatureCompat { + SignatureCompat { + signature: self.signature, + } + } + + /// Serializes the signature in compact format. + pub fn serialize_compact(&self) -> (K256RecoveryId, [u8; 64]) { + (self.recovery_id, self.signature.to_bytes().into()) + } +} + +/// Compatibility wrapper to provide missing methods. +pub struct SignatureCompat { + signature: K256Signature, +} + +impl SignatureCompat { + /// Serializes the signature in DER format. + pub fn serialize_der(&self) -> Vec { + self.signature.to_der().as_bytes().to_vec() + } +} + +impl From<(K256Signature, K256RecoveryId)> for RecoverableSignature { + fn from((signature, recovery_id): (K256Signature, K256RecoveryId)) -> Self { + RecoverableSignature { + signature, + recovery_id, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Secp256k1PublicKey { + key: K256VerifyingKey, + compressed: bool, +} +impl_byte_array_serde!(Secp256k1PublicKey); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Secp256k1PrivateKey { + key: K256SigningKey, + compress_public: bool, +} +impl_byte_array_serde!(Secp256k1PrivateKey); + +impl Hash for Secp256k1PublicKey { + fn hash(&self, state: &mut H) { + // Hash based on the compressed public key bytes for consistency + self.to_bytes_compressed().hash(state); + } +} + +impl MessageSignature { + /// Creates an "empty" signature (all zeros). Note this is not a valid signature. + pub fn empty() -> MessageSignature { + // NOTE: this cannot be a valid signature + MessageSignature([0u8; 65]) + } + + /// Generates place-holder data (for testing purposes only). + #[cfg(any(test, feature = "testing"))] + pub fn from_raw(sig: &[u8]) -> MessageSignature { + let mut buf = [0u8; 65]; + for (dst, src) in buf.iter_mut().zip(sig.iter().copied()) { + *dst = src; + } + MessageSignature(buf) + } + + /// Converts from a secp256k1::ecdsa::RecoverableSignature to our MessageSignature. + pub fn from_secp256k1_recoverable(sig: &RecoverableSignature) -> MessageSignature { + let (recid, bytes) = sig.serialize_compact(); + let mut ret_bytes = [0u8; 65]; + if let Some((first, rest)) = ret_bytes.split_first_mut() { + *first = recid.to_byte(); + rest.copy_from_slice(&bytes); + } + MessageSignature(ret_bytes) + } + + /// Converts to a secp256k1::ecdsa::RecoverableSignature. + pub fn to_secp256k1_recoverable(&self) -> Option { + let (recid_byte, sig_bytes) = self.0.split_first()?; + let recovery_id = K256RecoveryId::from_byte(*recid_byte)?; + let signature = K256Signature::from_slice(sig_bytes).ok()?; + Some(RecoverableSignature { + signature, + recovery_id, + }) + } + + /// Converts from VRS to RSV. + pub fn to_rsv(&self) -> Vec { + let mut out = self.0; + out.rotate_left(1); + out.to_vec() + } +} + +impl Secp256k1PublicKey { + /// Generates a new random public key (for testing purposes only). + #[cfg(any(test, feature = "testing"))] + pub fn new() -> Secp256k1PublicKey { + Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::random()) + } + + /// Creates a Secp256k1PublicKey from a hex string representation. + pub fn from_hex(hex_string: &str) -> Result { + let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex public key")?; + Secp256k1PublicKey::from_slice(&data[..]).map_err(|_e| "Invalid public key hex string") + } + + /// Creates a Secp256k1PublicKey from a byte slice. + pub fn from_slice(data: &[u8]) -> Result { + let encoded_point = EncodedPoint::from_bytes(data) + .map_err(|_| "Invalid public key: failed to parse encoded point")?; + + let public_key = + Option::::from(K256PublicKey::from_encoded_point(&encoded_point)) + .ok_or("Invalid public key: failed to decode point")?; + + let verifying_key = K256VerifyingKey::from(public_key); + + Ok(Secp256k1PublicKey { + key: verifying_key, + compressed: data.len() == 33, // 33 bytes = compressed, 65 bytes = uncompressed + }) + } + + /// Creates a Secp256k1PublicKey from a Secp256k1PrivateKey. + pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { + let verifying_key = privk.key.verifying_key(); + Secp256k1PublicKey { + key: *verifying_key, + compressed: privk.compress_public, + } + } + + /// Converts the public key to a hex string representation. + pub fn to_hex(&self) -> String { + to_hex(&self.to_bytes()) + } + + /// Converts the public key to a compressed byte representation. + pub fn to_bytes_compressed(&self) -> Vec { + let public_key = K256PublicKey::from(&self.key); + let encoded_point = public_key.to_encoded_point(true); // true = compressed + encoded_point.as_bytes().to_vec() + } + + /// Returns whether the public key is in compressed format. + pub fn compressed(&self) -> bool { + self.compressed + } + + /// Sets whether the public key should be in compressed format when serialized. + pub fn set_compressed(&mut self, value: bool) { + self.compressed = value; + } + + /// Recovers message and signature to public key (will be compressed). + pub fn recover_to_pubkey( + msg: &[u8], + sig: &MessageSignature, + ) -> Result { + if msg.len() != 32 { + return Err("Invalid message: failed to decode data hash: must be a 32-byte hash"); + } + + let recoverable_sig = sig + .to_secp256k1_recoverable() + .ok_or("Invalid signature: failed to decode recoverable signature")?; + + let recovered_key = K256VerifyingKey::recover_from_prehash( + msg, + &recoverable_sig.signature, + recoverable_sig.recovery_id, + ) + .map_err(|_| "Invalid signature: failed to recover public key")?; + + Ok(Secp256k1PublicKey { + key: recovered_key, + compressed: true, + }) + } + + // For benchmarking + #[cfg(test)] + pub fn recover_benchmark( + msg: &[u8; 32], + sig: &RecoverableSignature, + ) -> Result { + K256VerifyingKey::recover_from_prehash(msg, &sig.signature, sig.recovery_id) + .map_err(|_| "Invalid signature: failed to recover public key") + } +} + +#[cfg(any(test, feature = "testing"))] +impl Default for Secp256k1PublicKey { + fn default() -> Self { + Self::new() + } +} + +impl PublicKey for Secp256k1PublicKey { + /// Converts the public key to a byte representation. + fn to_bytes(&self) -> Vec { + let public_key = K256PublicKey::from(&self.key); + let encoded_point = public_key.to_encoded_point(self.compressed); + encoded_point.as_bytes().to_vec() + } + + /// Verifies a signature against the public key. + fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { + if data_hash.len() != 32 { + return Err("Invalid message: failed to decode data hash: must be a 32-byte hash"); + } + + let recoverable_sig = sig + .to_secp256k1_recoverable() + .ok_or("Invalid signature: failed to decode recoverable signature")?; + + let recovered_pubkey = K256VerifyingKey::recover_from_prehash( + data_hash, + &recoverable_sig.signature, + recoverable_sig.recovery_id, + ) + .map_err(|_| "Invalid signature: failed to recover public key")?; + + if recovered_pubkey != self.key { + test_debug!("{:?} != {:?}", &recovered_pubkey, &self.key); + return Ok(false); + } + + // Verify the signature is normalized (low-S) + if recoverable_sig.signature.normalize_s().is_some() { + return Err("Invalid signature: high-S"); + } + + Ok(true) + } +} + +impl Secp256k1PrivateKey { + /// Generates a new random private key. + #[cfg(feature = "rand")] + pub fn random() -> Secp256k1PrivateKey { + let secret_key = K256SecretKey::random(&mut rand::thread_rng()); + let signing_key = K256SigningKey::from(secret_key); + Secp256k1PrivateKey { + key: signing_key, + compress_public: true, + } + } + + /// Creates a new Secp256k1PrivateKey. + #[cfg(feature = "rand")] + pub fn new() -> Secp256k1PrivateKey { + Self::random() + } + + /// Creates a Secp256k1PrivateKey from seed bytes by repeatedly + /// SHA256 hashing the seed bytes until a private key is found. + /// + /// If `seed` is a valid private key, it will be returned without hashing. + /// The returned private key's compress_public flag will be `true`. + pub fn from_seed(seed: &[u8]) -> Secp256k1PrivateKey { + let mut re_hashed_seed = Vec::from(seed); + loop { + if let Ok(mut sk) = Secp256k1PrivateKey::from_slice(&re_hashed_seed[..]) { + // set this to true: LocalPeer will be doing this anyways, + // and that's currently the only way this method is used + sk.set_compress_public(true); + return sk; + } else { + re_hashed_seed = Sha256Sum::from_data(&re_hashed_seed[..]) + .as_bytes() + .to_vec() + } + } + } + + /// Creates a Secp256k1PrivateKey from a hex string representation. + pub fn from_hex(hex_string: &str) -> Result { + let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex private key")?; + Secp256k1PrivateKey::from_slice(&data[..]).map_err(|_e| "Invalid private key hex string") + } + + /// Creates a Secp256k1PrivateKey from a byte slice. + pub fn from_slice(data: &[u8]) -> Result { + if data.len() < 32 { + return Err("Invalid private key: shorter than 32 bytes"); + } + if data.len() > 33 { + return Err("Invalid private key: greater than 33 bytes"); + } + let compress_public = if data.len() == 33 { + // compressed byte tag? + if data[32] != 0x01 { + return Err("Invalid private key: invalid compressed byte marker"); + } + true + } else { + false + }; + + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(&data[0..32]); + + let secret_key = K256SecretKey::from_bytes(&GenericArray::from(key_bytes)) + .map_err(|_| "Invalid private key: failed to load")?; + let signing_key = K256SigningKey::from(secret_key); + + Ok(Secp256k1PrivateKey { + key: signing_key, + compress_public, + }) + } + + /// Returns whether the corresponding public key should be in compressed format when + /// serialized. + pub fn compress_public(&self) -> bool { + self.compress_public + } + + /// Sets whether the corresponding public key should be in compressed format when serialized. + pub fn set_compress_public(&mut self, value: bool) { + self.compress_public = value; + } + + /// Converts the private key to a hex string representation. + pub fn to_hex(&self) -> String { + let mut bytes = self.key.to_bytes().to_vec(); + if self.compress_public { + bytes.push(1); + } + to_hex(&bytes) + } + + /// Converts the private key to a 32-byte array representation. + pub fn as_slice(&self) -> [u8; 32] { + self.key.to_bytes().into() + } +} + +#[cfg(feature = "rand")] +impl Default for Secp256k1PrivateKey { + fn default() -> Self { + Self::new() + } +} + +impl PrivateKey for Secp256k1PrivateKey { + /// Converts the private key to a byte representation. + fn to_bytes(&self) -> Vec { + let mut bits = self.key.to_bytes().to_vec(); + if self.compress_public { + bits.push(0x01); + } + bits + } + + /// Signs a message hash with the private key, producing a recoverable signature. + fn sign(&self, data_hash: &[u8]) -> Result { + if data_hash.len() != 32 { + return Err("Invalid message: failed to decode data hash: must be a 32-byte hash"); + } + + let (signature, recovery_id) = self + .key + .sign_prehash_recoverable(data_hash) + .map_err(|_| "Signing failed")?; + + let recoverable_sig = RecoverableSignature { + signature, + recovery_id, + }; + + Ok(MessageSignature::from_secp256k1_recoverable( + &recoverable_sig, + )) + } +} + +/// Recovers a public key from a message hash and a recoverable signature. +/// The returned public key is in compressed format (33 bytes). +pub fn secp256k1_recover( + message_arr: &[u8], + serialized_signature_arr: &[u8], +) -> Result<[u8; 33], Secp256k1Error> { + if message_arr.len() != 32 { + return Err(Secp256k1Error::InvalidMessage); + } + + if serialized_signature_arr.len() < 65 { + return Err(Secp256k1Error::InvalidSignature); + } + + let recovery_id = K256RecoveryId::from_byte(serialized_signature_arr[64]) + .ok_or(Secp256k1Error::InvalidRecoveryId)?; + + let signature = K256Signature::from_slice(&serialized_signature_arr[..64]) + .map_err(|_| Secp256k1Error::InvalidSignature)?; + + let recovered_pub = + K256VerifyingKey::recover_from_prehash(message_arr, &signature, recovery_id) + .map_err(|_| Secp256k1Error::RecoveryFailed)?; + + let public_key = K256PublicKey::from(&recovered_pub); + let encoded_point = public_key.to_encoded_point(true); // compressed + let mut result = [0u8; 33]; + result.copy_from_slice(encoded_point.as_bytes()); + + Ok(result) +} + +/// Verifies a message hash against a signature and a public key. +pub fn secp256k1_verify( + message_arr: &[u8], + serialized_signature_arr: &[u8], + pubkey_arr: &[u8], +) -> Result<(), Secp256k1Error> { + if message_arr.len() != 32 { + return Err(Secp256k1Error::InvalidMessage); + } + + if serialized_signature_arr.len() < 64 { + return Err(Secp256k1Error::InvalidSignature); + } + + let encoded_point = + EncodedPoint::from_bytes(pubkey_arr).map_err(|_| Secp256k1Error::InvalidKey)?; + + let public_key = + Option::::from(K256PublicKey::from_encoded_point(&encoded_point)) + .ok_or(Secp256k1Error::InvalidKey)?; + let verifying_key = K256VerifyingKey::from(public_key); + + let signature = K256Signature::from_slice(&serialized_signature_arr[..64]) + .map_err(|_| Secp256k1Error::InvalidSignature)?; + + verifying_key + .verify_prehash(message_arr, &signature) + .map_err(|_| Secp256k1Error::InvalidSignature) +} + +#[cfg(test)] +mod tests { + use rand::RngCore; + + use super::*; + use crate::util::get_epoch_time_ms; + + struct KeyFixture { + input: I, + result: R, + } + + #[derive(Debug)] + struct VerifyFixture { + public_key: &'static str, + data: &'static str, + signature: &'static str, + result: R, + } + + fn verifying_key_from_hex(hex: &str) -> K256VerifyingKey { + let bytes = hex_bytes(hex).unwrap(); + K256VerifyingKey::from_sec1_bytes(&bytes).expect("valid verifying key bytes") + } + + #[test] + fn to_rsv_rotates_recovery_byte() { + let mut bytes = [0u8; 65]; + for (idx, slot) in bytes.iter_mut().enumerate() { + *slot = idx as u8; + } + let sig = MessageSignature(bytes); + + let rsv = sig.to_rsv(); + + assert_eq!(rsv.len(), 65); + assert_eq!(rsv[0], 1, "R should start where VRS's R begins"); + assert_eq!(rsv[63], 64, "S should end with last signature byte"); + assert_eq!(rsv[64], 0, "V should move to the tail after rotation"); + } + + #[test] + fn test_privkey_parse_serialize_compressed() { + let mut t1 = Secp256k1PrivateKey::random(); + t1.set_compress_public(true); + let h_comp = t1.to_hex(); + let json_comp = serde_json::to_string(&t1).unwrap(); + assert_eq!(json_comp, format!("\"{h_comp}\"")); + let deser_comp: Secp256k1PrivateKey = serde_json::from_str(&json_comp).unwrap(); + assert_eq!(deser_comp.to_hex(), h_comp); + assert!(deser_comp.compress_public()); + + t1.set_compress_public(false); + let h_uncomp = t1.to_hex(); + let json_uncomp = serde_json::to_string(&t1).unwrap(); + assert_eq!(json_uncomp, format!("\"{h_uncomp}\"")); + let deser_uncomp: Secp256k1PrivateKey = serde_json::from_str(&json_uncomp).unwrap(); + assert_eq!(deser_uncomp.to_hex(), h_uncomp); + assert!(!deser_uncomp.compress_public()); + + assert!(h_comp != h_uncomp); + assert_eq!(h_comp.len(), 66); + assert_eq!(h_uncomp.len(), 64); + + let (uncomp, comp_value) = h_comp.split_at(64); + assert_eq!(comp_value, "01"); + assert_eq!(uncomp, &h_uncomp); + + assert!(Secp256k1PrivateKey::from_hex(&h_comp) + .unwrap() + .compress_public()); + assert!(!Secp256k1PrivateKey::from_hex(&h_uncomp) + .unwrap() + .compress_public()); + + assert_eq!(Secp256k1PrivateKey::from_hex(&h_uncomp), Ok(t1.clone())); + + t1.set_compress_public(true); + + assert_eq!(Secp256k1PrivateKey::from_hex(&h_comp), Ok(t1)); + } + + #[test] + fn test_pubkey_parse_serialize_compressed() { + let privk = Secp256k1PrivateKey::random(); + let mut pubk = Secp256k1PublicKey::from_private(&privk); + + pubk.set_compressed(true); + let h_comp = pubk.to_hex(); + let json_comp = serde_json::to_string(&pubk).unwrap(); + assert_eq!(json_comp, format!("\"{}\"", h_comp)); + let deser_comp: Secp256k1PublicKey = serde_json::from_str(&json_comp).unwrap(); + assert_eq!(deser_comp.to_hex(), h_comp); + assert!(deser_comp.compressed()); + + pubk.set_compressed(false); + let h_uncomp = pubk.to_hex(); + let json_uncomp = serde_json::to_string(&pubk).unwrap(); + assert_eq!(json_uncomp, format!("\"{}\"", h_uncomp)); + let deser_uncomp: Secp256k1PublicKey = serde_json::from_str(&json_uncomp).unwrap(); + assert_eq!(deser_uncomp.to_hex(), h_uncomp); + assert!(!deser_uncomp.compressed()); + + assert!(h_comp != h_uncomp); + assert_eq!(h_comp.len(), 66); + assert_eq!(h_uncomp.len(), 130); + + assert!(Secp256k1PublicKey::from_hex(&h_comp).unwrap().compressed()); + assert!(!Secp256k1PublicKey::from_hex(&h_uncomp) + .unwrap() + .compressed()); + + assert_eq!(Secp256k1PublicKey::from_hex(&h_uncomp), Ok(pubk.clone())); + + pubk.set_compressed(true); + + assert_eq!(Secp256k1PublicKey::from_hex(&h_comp), Ok(pubk)); + } + + #[test] + /// Test the behavior of from_seed using hard-coded values from previous existing integration tests + fn sk_from_seed() { + let sk = Secp256k1PrivateKey::from_seed(&[2; 32]); + assert_eq!( + Secp256k1PublicKey::from_private(&sk).to_hex(), + "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + ); + assert_eq!( + sk.to_hex(), + "020202020202020202020202020202020202020202020202020202020202020201" + ); + + let sk = Secp256k1PrivateKey::from_seed(&[0]); + assert_eq!( + Secp256k1PublicKey::from_private(&sk).to_hex(), + "0243311589af63c2adda04fcd7792c038a05c12a4fe40351b3eb1612ff6b2e5a0e" + ); + assert_eq!( + sk.to_hex(), + "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d01" + ); + } + + #[test] + fn test_parse_serialize() { + let fixtures = vec![ + KeyFixture { + input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5", + result: Some(Secp256k1PublicKey { + key: verifying_key_from_hex("0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5"), + compressed: true + }) + }, + KeyFixture { + input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7", + result: Some(Secp256k1PublicKey { + key: verifying_key_from_hex("044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7"), + compressed: false + }) + }, + KeyFixture { + input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12ce", + result: None, + }, + KeyFixture { + input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8a", + result: None, + } + ]; + + for fixture in fixtures { + let key_res = Secp256k1PublicKey::from_hex(fixture.input); + match (key_res, fixture.result) { + (Ok(key), Some(key_result)) => { + assert_eq!(key, key_result); + + let key_from_slice = + Secp256k1PublicKey::from_slice(&hex_bytes(fixture.input).unwrap()[..]) + .unwrap(); + assert_eq!(key_from_slice, key_result); + + let key_bytes = key.to_bytes(); + assert_eq!(key_bytes, hex_bytes(fixture.input).unwrap()); + } + (Err(_e), None) => {} + (_, _) => { + // either got a key when we didn't expect one, or didn't get a key when we did + // expect one. + panic!("Unexpected result: we either got a key when we didn't expect one, or didn't get a key when we did expect one."); + } + } + } + } + + #[test] + fn test_verify() { + let fixtures : Vec>> = vec![ + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Ok(true) + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + data: "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce", // sha256 hash of "nope" + result: Ok(false) + }, + VerifyFixture { + public_key: "034c35b09b758678165d6ed84a50b329900c99986cf8e9a358ceae0d03af91f5b6", // wrong key + signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Ok(false) + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe7", // wrong sig (bad s) + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Ok(false) + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "00454445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad r) + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Ok(false) + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "01354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Ok(false) + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "02354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Err("Invalid signature: failed to recover public key"), + }, + VerifyFixture { + public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", + signature: "03354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) + data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" + result: Err("Invalid signature: failed to recover public key"), + } + ]; + + for fixture in fixtures { + let key = Secp256k1PublicKey::from_hex(fixture.public_key).unwrap(); + let signature = MessageSignature::from_raw(&hex_bytes(fixture.signature).unwrap()); + let ver_res = key.verify(&hex_bytes(fixture.data).unwrap(), &signature); + match (ver_res, fixture.result) { + (Ok(true), Ok(true)) => {} + (Ok(false), Ok(false)) => {} + (Err(e1), Err(e2)) => assert_eq!(e1, e2), + (Err(e1), _) => { + test_debug!("Failed to verify signature: {}", e1); + panic!( + "failed fixture (verification: {:?}): {:#?}", + &ver_res, &fixture + ); + } + (_, _) => { + panic!( + "failed fixture (verification: {:?}): {:#?}", + &ver_res, &fixture + ); + } + } + } + } + + #[test] + #[ignore] + fn test_verify_benchmark_roundtrip() { + let mut runtime_sign = 0; + let mut runtime_verify = 0; + let mut runtime_recover = 0; + let mut rng = rand::thread_rng(); + + for i in 0..100 { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + + let mut msg = [0u8; 32]; + rng.fill_bytes(&mut msg); + + let sign_start = get_epoch_time_ms(); + for i in 0..1000 { + let sig = privk.sign(&msg).unwrap(); + } + let sign_end = get_epoch_time_ms(); + + let sig = privk.sign(&msg).unwrap(); + let recoverable_sig = sig.to_secp256k1_recoverable().unwrap(); + + let recovered_pubk = + Secp256k1PublicKey::recover_benchmark(&msg, &recoverable_sig).unwrap(); + assert_eq!(recovered_pubk, pubk.key); + + let recover_start = get_epoch_time_ms(); + for i in 0..1000 { + let recovered_pubk = + Secp256k1PublicKey::recover_benchmark(&msg, &recoverable_sig).unwrap(); + } + let recover_end = get_epoch_time_ms(); + + let verify_start = get_epoch_time_ms(); + for i in 0..1000 { + let valid = pubk.verify(&msg, &sig).unwrap(); + } + let verify_end = get_epoch_time_ms(); + + let valid = pubk.verify(&msg, &sig).unwrap(); + assert!(valid); + + test_debug!( + "Runtime: {:?} sign, {:?} recover, {:?} verify", + ((sign_end - sign_start) as f64) / 1000.0, + ((recover_end - recover_start) as f64) / 1000.0, + ((verify_end - verify_start) as f64) / 1000.0 + ); + + runtime_sign += sign_end - sign_start; + runtime_verify += verify_end - verify_start; + runtime_recover += recover_end - recover_start; + } + + test_debug!( + "Total Runtime: {:?} sign, {:?} verify, {:?} recover, {:?} verify - recover", + runtime_sign, + runtime_verify, + runtime_recover, + runtime_verify - runtime_recover + ); + } + + #[test] + fn test_from_seed() { + let sk = Secp256k1PrivateKey::from_seed(&[2; 32]); + let pubk = Secp256k1PublicKey::from_private(&sk); + + // Test that from_seed is deterministic + let sk2 = Secp256k1PrivateKey::from_seed(&[2; 32]); + let pubk2 = Secp256k1PublicKey::from_private(&sk2); + + assert_eq!(sk.to_hex(), sk2.to_hex()); + assert_eq!(pubk.to_hex(), pubk2.to_hex()); + } + + #[test] + fn test_roundtrip_sign_verify() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + + let msg = b"hello world"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + let sig = privk.sign(&msg_hash).unwrap(); + let valid = pubk.verify(&msg_hash, &sig).unwrap(); + + assert!(valid); + } + + #[test] + fn test_verify_with_different_key() { + let privk1 = Secp256k1PrivateKey::random(); + let privk2 = Secp256k1PrivateKey::random(); + let pubk2 = Secp256k1PublicKey::from_private(&privk2); + + let msg = b"hello world"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + let sig = privk1.sign(&msg_hash).unwrap(); + let valid = pubk2.verify(&msg_hash, &sig).unwrap(); + + assert!(!valid); + } + + #[test] + fn test_public_key_compression() { + let privk = Secp256k1PrivateKey::random(); + let mut pubk = Secp256k1PublicKey::from_private(&privk); + + pubk.set_compressed(true); + let compressed_bytes = pubk.to_bytes(); + assert_eq!(compressed_bytes.len(), 33); + + pubk.set_compressed(false); + let uncompressed_bytes = pubk.to_bytes(); + assert_eq!(uncompressed_bytes.len(), 65); + + // Both should parse back to the same key + let pubk_from_compressed = Secp256k1PublicKey::from_slice(&compressed_bytes).unwrap(); + let pubk_from_uncompressed = Secp256k1PublicKey::from_slice(&uncompressed_bytes).unwrap(); + + assert_eq!(pubk_from_compressed.key, pubk_from_uncompressed.key); + } + + #[test] + fn test_recovery() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + + let msg = b"hello world"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + let sig = privk.sign(&msg_hash).unwrap(); + let recovered_pubk = Secp256k1PublicKey::recover_to_pubkey(&msg_hash, &sig).unwrap(); + + // Both should have the same compressed public key bytes + assert_eq!( + pubk.to_bytes_compressed(), + recovered_pubk.to_bytes_compressed() + ); + } + + #[test] + fn test_high_s_signature() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + + let msg = b"stacks secp256k1 high-s test____"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + // Sign the message + let sig = privk.sign(&msg_hash).unwrap(); + let pubkey_bytes = pubk.to_bytes(); + + // Get the underlying Signature to work with r,s components + let recoverable_sig = sig.to_secp256k1_recoverable().unwrap(); + + // Always get the low-s version first + let low_sig = if let Some(normalized) = recoverable_sig.signature.normalize_s() { + normalized // Original was high-s, use the normalized (low-s) version + } else { + recoverable_sig.signature // Original was already low-s + }; + + // Now create high-s version from the low-s signature + let (r, s) = (low_sig.r(), low_sig.s()); + let high_sig = { + // Make high-s by negating s (s' = -s mod n) + let s_hi = -(*s); + K256Signature::from_scalars(*r, s_hi).expect("valid (r, -s)") + }; + + let low_bytes = low_sig.to_bytes(); + let high_bytes = high_sig.to_bytes(); + + // Verify our assumptions about which is which + let low_is_low_s = low_sig.normalize_s().is_none(); + let high_is_high_s = high_sig.normalize_s().is_some(); + + assert!(low_is_low_s, "Low signature should be low-s"); + assert!(high_is_high_s, "High signature should be high-s"); + + // Low-s signature should pass verification + let low_result = secp256k1_verify(&msg_hash, &low_bytes, &pubkey_bytes); + assert!( + low_result.is_ok(), + "Low-s signature should pass verification" + ); + + // High-s signature should fail verification + let high_result = secp256k1_verify(&msg_hash, &high_bytes, &pubkey_bytes); + assert!( + high_result.is_err(), + "High-s signature should fail verification" + ); + + // Test normalization: high-s should pass when normalized to low-s + if let Some(normalized_sig) = high_sig.normalize_s() { + let normalized_bytes = normalized_sig.to_bytes(); + let normalized_result = secp256k1_verify(&msg_hash, &normalized_bytes, &pubkey_bytes); + assert!( + normalized_result.is_ok(), + "Normalized (low-s) signature should pass verification" + ); + + // The normalized signature should be the same as our low signature + assert_eq!( + normalized_bytes, low_bytes, + "Normalized signature should match our low signature" + ); + } else { + panic!("High-s signature should normalize to low-s"); + } + } +} diff --git a/stacks-common/src/util/secp256k1/mod.rs b/stacks-common/src/util/secp256k1/mod.rs deleted file mode 100644 index 50ee281e306..00000000000 --- a/stacks-common/src/util/secp256k1/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[cfg(not(target_family = "wasm"))] -mod native; - -#[cfg(not(target_family = "wasm"))] -pub use self::native::*; - -#[cfg(target_family = "wasm")] -mod wasm; - -#[cfg(target_family = "wasm")] -pub use self::wasm::*; - -pub const MESSAGE_SIGNATURE_ENCODED_SIZE: u32 = 65; - -pub struct MessageSignature(pub [u8; 65]); -impl_array_newtype!(MessageSignature, u8, 65); -impl_array_hexstring_fmt!(MessageSignature); -impl_byte_array_newtype!(MessageSignature, u8, 65); -impl_byte_array_serde!(MessageSignature); - -pub struct SchnorrSignature(pub [u8; 65]); -impl_array_newtype!(SchnorrSignature, u8, 65); -impl_array_hexstring_fmt!(SchnorrSignature); -impl_byte_array_newtype!(SchnorrSignature, u8, 65); -impl_byte_array_serde!(SchnorrSignature); -pub const SCHNORR_SIGNATURE_ENCODED_SIZE: u32 = 65; - -impl Default for SchnorrSignature { - /// Creates a default Schnorr Signature. Note this is not a valid signature. - fn default() -> Self { - Self([0u8; 65]) - } -} diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs deleted file mode 100644 index e6b7452061d..00000000000 --- a/stacks-common/src/util/secp256k1/native.rs +++ /dev/null @@ -1,714 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ::secp256k1; -use ::secp256k1::ecdsa::{ - RecoverableSignature as LibSecp256k1RecoverableSignature, RecoveryId as LibSecp256k1RecoveryID, - Signature as LibSecp256k1Signature, -}; -pub use ::secp256k1::Error; -use ::secp256k1::{ - constants as LibSecp256k1Constants, Error as LibSecp256k1Error, Message as LibSecp256k1Message, - PublicKey as LibSecp256k1PublicKey, Secp256k1, SecretKey as LibSecp256k1PrivateKey, -}; -use serde::de::{Deserialize, Error as de_Error}; -use serde::Serialize; - -use super::MessageSignature; -use crate::types::{PrivateKey, PublicKey}; -use crate::util::hash::{hex_bytes, to_hex, Sha256Sum}; - -// per-thread Secp256k1 context -thread_local!(static _secp256k1: Secp256k1 = Secp256k1::new()); - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash)] -pub struct Secp256k1PublicKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_pubkey_serialize", - deserialize_with = "secp256k1_pubkey_deserialize" - )] - key: LibSecp256k1PublicKey, - compressed: bool, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Secp256k1PrivateKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_privkey_serialize", - deserialize_with = "secp256k1_privkey_deserialize" - )] - key: LibSecp256k1PrivateKey, - compress_public: bool, -} - -impl MessageSignature { - pub fn empty() -> MessageSignature { - // NOTE: this cannot be a valid signature - MessageSignature([0u8; 65]) - } - - #[cfg(any(test, feature = "testing"))] - // test method for generating place-holder data - pub fn from_raw(sig: &[u8]) -> MessageSignature { - let mut buf = [0u8; 65]; - if sig.len() < 65 { - buf.copy_from_slice(sig); - } else { - buf.copy_from_slice(&sig[..65]); - } - MessageSignature(buf) - } - - pub fn from_secp256k1_recoverable(sig: &LibSecp256k1RecoverableSignature) -> MessageSignature { - let (recid, bytes) = sig.serialize_compact(); - let mut ret_bytes = [0u8; 65]; - let recovery_id_byte = recid.to_i32() as u8; // recovery ID will be 0, 1, 2, or 3 - ret_bytes[0] = recovery_id_byte; - ret_bytes[1..=64].copy_from_slice(&bytes[..64]); - MessageSignature(ret_bytes) - } - - pub fn to_secp256k1_recoverable(&self) -> Option { - let recid = match LibSecp256k1RecoveryID::from_i32(self.0[0] as i32) { - Ok(rid) => rid, - Err(_) => { - return None; - } - }; - let mut sig_bytes = [0u8; 64]; - sig_bytes[..64].copy_from_slice(&self.0[1..=64]); - - LibSecp256k1RecoverableSignature::from_compact(&sig_bytes, recid).ok() - } - - /// Convert from VRS to RSV - pub fn to_rsv(&self) -> Vec { - [&self.0[1..], &self.0[0..1]].concat() - } -} - -#[cfg(any(test, feature = "testing"))] -impl Default for Secp256k1PublicKey { - fn default() -> Self { - Self::new() - } -} - -impl Secp256k1PublicKey { - #[cfg(any(test, feature = "testing"))] - pub fn new() -> Secp256k1PublicKey { - Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::random()) - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex public key")?; - Secp256k1PublicKey::from_slice(&data[..]).map_err(|_e| "Invalid public key hex string") - } - - pub fn from_slice(data: &[u8]) -> Result { - match LibSecp256k1PublicKey::from_slice(data) { - Ok(pubkey_res) => Ok(Secp256k1PublicKey { - key: pubkey_res, - compressed: data.len() == LibSecp256k1Constants::PUBLIC_KEY_SIZE, - }), - Err(_e) => Err("Invalid public key: failed to load"), - } - } - - pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { - _secp256k1.with(|ctx| { - let pubk = LibSecp256k1PublicKey::from_secret_key(ctx, &privk.key); - Secp256k1PublicKey { - key: pubk, - compressed: privk.compress_public, - } - }) - } - - pub fn to_hex(&self) -> String { - to_hex(&self.to_bytes()) - } - - pub fn to_bytes_compressed(&self) -> Vec { - self.key.serialize().to_vec() - } - - pub fn compressed(&self) -> bool { - self.compressed - } - - pub fn set_compressed(&mut self, value: bool) { - self.compressed = value; - } - - /// recover message and signature to public key (will be compressed) - pub fn recover_to_pubkey( - msg: &[u8], - sig: &MessageSignature, - ) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(msg).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let secp256k1_sig = sig - .to_secp256k1_recoverable() - .ok_or("Invalid signature: failed to decode recoverable signature")?; - - let recovered_pubkey = ctx - .recover_ecdsa(&msg, &secp256k1_sig) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - Ok(Secp256k1PublicKey { - key: recovered_pubkey, - compressed: true, - }) - }) - } - - // for benchmarking - #[cfg(test)] - pub fn recover_benchmark( - msg: &LibSecp256k1Message, - sig: &LibSecp256k1RecoverableSignature, - ) -> Result { - _secp256k1.with(|ctx| { - ctx.recover_ecdsa(msg, sig) - .map_err(|_e| "Invalid signature: failed to recover public key") - }) - } -} - -impl PublicKey for Secp256k1PublicKey { - fn to_bytes(&self) -> Vec { - if self.compressed { - self.key.serialize().to_vec() - } else { - self.key.serialize_uncompressed().to_vec() - } - } - - fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let secp256k1_sig = sig - .to_secp256k1_recoverable() - .ok_or("Invalid signature: failed to decode recoverable signature")?; - - let recovered_pubkey = ctx - .recover_ecdsa(&msg, &secp256k1_sig) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - if recovered_pubkey != self.key { - test_debug!("{:?} != {:?}", &recovered_pubkey, &self.key); - return Ok(false); - } - - // NOTE: libsecp256k1 _should_ ensure that the S is low, - // but add this check just to be safe. - let secp256k1_sig_standard = secp256k1_sig.to_standard(); - - // must be low-S - let mut secp256k1_sig_low_s = secp256k1_sig_standard; - secp256k1_sig_low_s.normalize_s(); - if secp256k1_sig_low_s != secp256k1_sig_standard { - return Err("Invalid signature: high-S"); - } - - Ok(true) - }) - } -} - -impl Secp256k1PrivateKey { - #[cfg(feature = "rand")] - pub fn random() -> Secp256k1PrivateKey { - use rand::RngCore as _; - - let mut rng = rand::thread_rng(); - loop { - // keep trying to generate valid bytes - let mut random_32_bytes = [0u8; 32]; - rng.fill_bytes(&mut random_32_bytes); - let pk_res = LibSecp256k1PrivateKey::from_slice(&random_32_bytes); - match pk_res { - Ok(pk) => { - return Secp256k1PrivateKey { - key: pk, - compress_public: true, - }; - } - Err(_) => { - continue; - } - } - } - } - - /// Create a Secp256k1PrivateKey from seed bytes by repeatedly - /// SHA256 hashing the seed bytes until a private key is found. - /// - /// If `seed` is a valid private key, it will be returned without hashing. - /// The returned private key's compress_public flag will be `true` - pub fn from_seed(seed: &[u8]) -> Secp256k1PrivateKey { - let mut re_hashed_seed = Vec::from(seed); - loop { - if let Ok(mut sk) = Secp256k1PrivateKey::from_slice(&re_hashed_seed[..]) { - // set this to true: LocalPeer will be doing this anyways, - // and that's currently the only way this method is used - sk.set_compress_public(true); - return sk; - } else { - re_hashed_seed = Sha256Sum::from_data(&re_hashed_seed[..]) - .as_bytes() - .to_vec() - } - } - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex private key")?; - Secp256k1PrivateKey::from_slice(&data[..]).map_err(|_e| "Invalid private key hex string") - } - - pub fn from_slice(data: &[u8]) -> Result { - if data.len() < 32 { - return Err("Invalid private key: shorter than 32 bytes"); - } - if data.len() > 33 { - return Err("Invalid private key: greater than 33 bytes"); - } - let compress_public = if data.len() == 33 { - // compressed byte tag? - if data[32] != 0x01 { - return Err("Invalid private key: invalid compressed byte marker"); - } - true - } else { - false - }; - match LibSecp256k1PrivateKey::from_slice(&data[0..32]) { - Ok(privkey_res) => Ok(Secp256k1PrivateKey { - key: privkey_res, - compress_public, - }), - Err(_e) => Err("Invalid private key: failed to load"), - } - } - - pub fn compress_public(&self) -> bool { - self.compress_public - } - - pub fn set_compress_public(&mut self, value: bool) { - self.compress_public = value; - } - - pub fn to_hex(&self) -> String { - let mut bytes = self.key[..].to_vec(); - if self.compress_public { - bytes.push(1); - } - to_hex(&bytes) - } - - pub fn as_slice(&self) -> &[u8; 32] { - self.key.as_ref() - } -} - -impl PrivateKey for Secp256k1PrivateKey { - fn to_bytes(&self) -> Vec { - let mut bits = self.key[..].to_vec(); - if self.compress_public { - bits.push(0x01); - } - bits - } - - fn sign(&self, data_hash: &[u8]) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let sig = ctx.sign_ecdsa_recoverable(&msg, &self.key); - Ok(MessageSignature::from_secp256k1_recoverable(&sig)) - }) - } - - #[cfg(any(test, feature = "testing"))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let sig = ctx.sign_ecdsa_recoverable_with_noncedata(&msg, &self.key, noncedata); - Ok(MessageSignature::from_secp256k1_recoverable(&sig)) - }) - } -} - -fn secp256k1_pubkey_serialize( - pubk: &LibSecp256k1PublicKey, - s: S, -) -> Result { - let key_hex = to_hex(&pubk.serialize()); - s.serialize_str(key_hex.as_str()) -} - -fn secp256k1_pubkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PublicKey::from_slice(&key_bytes).map_err(de_Error::custom) -} - -fn secp256k1_privkey_serialize( - privk: &LibSecp256k1PrivateKey, - s: S, -) -> Result { - let key_hex = to_hex(&privk[..]); - s.serialize_str(key_hex.as_str()) -} - -fn secp256k1_privkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PrivateKey::from_slice(&key_bytes[..]).map_err(de_Error::custom) -} - -pub fn secp256k1_recover( - message_arr: &[u8], - serialized_signature_arr: &[u8], -) -> Result<[u8; 33], LibSecp256k1Error> { - _secp256k1.with(|ctx| { - let message = LibSecp256k1Message::from_slice(message_arr)?; - - let rec_id = LibSecp256k1RecoveryID::from_i32(serialized_signature_arr[64] as i32)?; - let recovered_sig = LibSecp256k1RecoverableSignature::from_compact( - &serialized_signature_arr[..64], - rec_id, - )?; - let recovered_pub = ctx.recover_ecdsa(&message, &recovered_sig)?; - let recovered_serialized = recovered_pub.serialize(); // 33 bytes version - - Ok(recovered_serialized) - }) -} - -pub fn secp256k1_verify( - message_arr: &[u8], - serialized_signature_arr: &[u8], - pubkey_arr: &[u8], -) -> Result<(), LibSecp256k1Error> { - _secp256k1.with(|ctx| { - let message = LibSecp256k1Message::from_slice(message_arr)?; - let expanded_sig = LibSecp256k1Signature::from_compact(&serialized_signature_arr[..64])?; // ignore 65th byte if present - let pubkey = LibSecp256k1PublicKey::from_slice(pubkey_arr)?; - ctx.verify_ecdsa(&message, &expanded_sig, &pubkey) - }) -} - -#[cfg(test)] -mod tests { - use rand::RngCore as _; - use secp256k1; - use secp256k1::{PublicKey as LibSecp256k1PublicKey, Secp256k1}; - - use super::*; - use crate::util::get_epoch_time_ms; - use crate::util::hash::hex_bytes; - - struct KeyFixture { - input: I, - result: R, - } - - #[derive(Debug)] - struct VerifyFixture { - public_key: &'static str, - data: &'static str, - signature: &'static str, - result: R, - } - - #[test] - fn test_parse_serialize_compressed() { - let mut t1 = Secp256k1PrivateKey::random(); - t1.set_compress_public(true); - let h_comp = t1.to_hex(); - t1.set_compress_public(false); - let h_uncomp = t1.to_hex(); - - assert!(h_comp != h_uncomp); - assert_eq!(h_comp.len(), 66); - assert_eq!(h_uncomp.len(), 64); - - let (uncomp, comp_value) = h_comp.split_at(64); - assert_eq!(comp_value, "01"); - assert_eq!(uncomp, &h_uncomp); - - assert!(Secp256k1PrivateKey::from_hex(&h_comp) - .unwrap() - .compress_public()); - assert!(!Secp256k1PrivateKey::from_hex(&h_uncomp) - .unwrap() - .compress_public()); - - assert_eq!(Secp256k1PrivateKey::from_hex(&h_uncomp), Ok(t1.clone())); - - t1.set_compress_public(true); - - assert_eq!(Secp256k1PrivateKey::from_hex(&h_comp), Ok(t1)); - } - - #[test] - /// Test the behavior of from_seed using hard-coded values from previous existing integration tests - fn sk_from_seed() { - let sk = Secp256k1PrivateKey::from_seed(&[2; 32]); - assert_eq!( - Secp256k1PublicKey::from_private(&sk).to_hex(), - "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" - ); - assert_eq!( - sk.to_hex(), - "020202020202020202020202020202020202020202020202020202020202020201" - ); - - let sk = Secp256k1PrivateKey::from_seed(&[0]); - assert_eq!( - Secp256k1PublicKey::from_private(&sk).to_hex(), - "0243311589af63c2adda04fcd7792c038a05c12a4fe40351b3eb1612ff6b2e5a0e" - ); - assert_eq!( - sk.to_hex(), - "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d01" - ); - } - - #[test] - fn test_parse_serialize() { - let ctx: Secp256k1 = Secp256k1::new(); - let fixtures = vec![ - KeyFixture { - input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5", - result: Some(Secp256k1PublicKey { - key: LibSecp256k1PublicKey::from_slice(&hex_bytes("0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5").unwrap()[..]).unwrap(), - compressed: true - }) - }, - KeyFixture { - input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7", - result: Some(Secp256k1PublicKey { - key: LibSecp256k1PublicKey::from_slice(&hex_bytes("044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7").unwrap()[..]).unwrap(), - compressed: false - }) - }, - KeyFixture { - input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12ce", - result: None, - }, - KeyFixture { - input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8a", - result: None, - } - ]; - - for fixture in fixtures { - let key_res = Secp256k1PublicKey::from_hex(fixture.input); - match (key_res, fixture.result) { - (Ok(key), Some(key_result)) => { - assert_eq!(key, key_result); - - let key_from_slice = - Secp256k1PublicKey::from_slice(&hex_bytes(fixture.input).unwrap()[..]) - .unwrap(); - assert_eq!(key_from_slice, key_result); - - let key_bytes = key.to_bytes(); - assert_eq!(key_bytes, hex_bytes(fixture.input).unwrap()); - } - (Err(_e), None) => {} - (_, _) => { - // either got a key when we didn't expect one, or didn't get a key when we did - // expect one. - panic!("Unexpected result: we either got a key when we didn't expect one, or didn't get a key when we did expect one."); - } - } - } - } - - #[test] - fn test_verify() { - let _ctx: Secp256k1 = Secp256k1::new(); - let fixtures : Vec>> = vec![ - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Ok(true) - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", - data: "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce", // sha256 hash of "nope" - result: Ok(false) - }, - VerifyFixture { - public_key: "034c35b09b758678165d6ed84a50b329900c99986cf8e9a358ceae0d03af91f5b6", // wrong key - signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Ok(false) - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe7", // wrong sig (bad s) - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Ok(false) - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "00454445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad r) - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Ok(false) - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "01354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Ok(false) - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "02354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Err("Invalid signature: failed to recover public key"), - }, - VerifyFixture { - public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", - signature: "03354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", // wrong sig (bad recovery) - data: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", // sha256 hash of "hello world" - result: Err("Invalid signature: failed to recover public key"), - } - ]; - - for fixture in fixtures { - let key = Secp256k1PublicKey::from_hex(fixture.public_key).unwrap(); - let signature = MessageSignature::from_raw(&hex_bytes(fixture.signature).unwrap()); - let ver_res = key.verify(&hex_bytes(fixture.data).unwrap(), &signature); - match (ver_res, fixture.result) { - (Ok(true), Ok(true)) => {} - (Ok(false), Ok(false)) => {} - (Err(e1), Err(e2)) => assert_eq!(e1, e2), - (Err(e1), _) => { - test_debug!("Failed to verify signature: {}", e1); - panic!( - "failed fixture (verification: {:?}): {:#?}", - &ver_res, &fixture - ); - } - (_, _) => { - panic!( - "failed fixture (verification: {:?}): {:#?}", - &ver_res, &fixture - ); - } - } - } - } - - #[test] - #[ignore] - fn test_verify_benchmark_roundtrip() { - let mut runtime_sign = 0; - let mut runtime_verify = 0; - let mut runtime_recover = 0; - let mut rng = rand::thread_rng(); - - for i in 0..100 { - let privk = Secp256k1PrivateKey::random(); - let pubk = Secp256k1PublicKey::from_private(&privk); - - let mut msg = [0u8; 32]; - rng.fill_bytes(&mut msg); - - let sign_start = get_epoch_time_ms(); - for i in 0..1000 { - let sig = privk.sign(&msg).unwrap(); - } - let sign_end = get_epoch_time_ms(); - - let sig = privk.sign(&msg).unwrap(); - let secp256k1_msg = LibSecp256k1Message::from_slice(&msg).unwrap(); - let secp256k1_sig = sig.to_secp256k1_recoverable().unwrap(); - - let recovered_pubk = - Secp256k1PublicKey::recover_benchmark(&secp256k1_msg, &secp256k1_sig).unwrap(); - assert_eq!(recovered_pubk, pubk.key); - - let recover_start = get_epoch_time_ms(); - for i in 0..1000 { - let recovered_pubk = - Secp256k1PublicKey::recover_benchmark(&secp256k1_msg, &secp256k1_sig).unwrap(); - } - let recover_end = get_epoch_time_ms(); - - let verify_start = get_epoch_time_ms(); - for i in 0..1000 { - let valid = pubk.verify(&msg, &sig).unwrap(); - } - let verify_end = get_epoch_time_ms(); - - let valid = pubk.verify(&msg, &sig).unwrap(); - assert!(valid); - - test_debug!( - "Runtime: {:?} sign, {:?} recover, {:?} verify", - ((sign_end - sign_start) as f64) / 1000.0, - ((recover_end - recover_start) as f64) / 1000.0, - ((verify_end - verify_start) as f64) / 1000.0 - ); - - runtime_sign += sign_end - sign_start; - runtime_verify += verify_end - verify_start; - runtime_recover += recover_end - recover_start; - } - - test_debug!( - "Total Runtime: {:?} sign, {:?} verify, {:?} recover, {:?} verify - recover", - runtime_sign, - runtime_verify, - runtime_recover, - runtime_verify - runtime_recover - ); - } -} diff --git a/stacks-common/src/util/secp256k1/wasm.rs b/stacks-common/src/util/secp256k1/wasm.rs deleted file mode 100644 index 55b5266bfd5..00000000000 --- a/stacks-common/src/util/secp256k1/wasm.rs +++ /dev/null @@ -1,383 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ::libsecp256k1; -use ::libsecp256k1::curve::Scalar; -pub use ::libsecp256k1::Error; -#[cfg(not(feature = "wasm-deterministic"))] -use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; -use ::libsecp256k1::{ - PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, - SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, -}; -use serde::de::{Deserialize, Error as de_Error}; -use serde::Serialize; - -use super::MessageSignature; -use crate::types::{PrivateKey, PublicKey}; -use crate::util::hash::{hex_bytes, to_hex}; - -pub const PUBLIC_KEY_SIZE: usize = 33; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Secp256k1PublicKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_pubkey_serialize", - deserialize_with = "secp256k1_pubkey_deserialize" - )] - key: LibSecp256k1PublicKey, - compressed: bool, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct Secp256k1PrivateKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_privkey_serialize", - deserialize_with = "secp256k1_privkey_deserialize" - )] - key: LibSecp256k1PrivateKey, - compress_public: bool, -} - -impl Secp256k1PublicKey { - pub fn from_slice(data: &[u8]) -> Result { - let (format, compressed) = if data.len() == PUBLIC_KEY_SIZE { - (libsecp256k1::PublicKeyFormat::Compressed, true) - } else { - (libsecp256k1::PublicKeyFormat::Full, false) - }; - match LibSecp256k1PublicKey::parse_slice(data, Some(format)) { - Ok(pubkey_res) => Ok(Secp256k1PublicKey { - key: pubkey_res, - compressed, - }), - Err(_e) => Err("Invalid public key: failed to load"), - } - } - - pub fn to_hex(&self) -> String { - if self.compressed { - to_hex(&self.key.serialize_compressed().to_vec()) - } else { - to_hex(&self.key.serialize().to_vec()) - } - } - - pub fn to_bytes_compressed(&self) -> Vec { - self.key.serialize_compressed().to_vec() - } - - pub fn compressed(&self) -> bool { - self.compressed - } - - pub fn set_compressed(&mut self, value: bool) { - self.compressed = value; - } - - pub fn to_bytes(&self) -> Vec { - if self.compressed { - self.key.serialize_compressed().to_vec() - } else { - self.key.serialize().to_vec() - } - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex public key")?; - Secp256k1PublicKey::from_slice(&data[..]).map_err(|_e| "Invalid public key hex string") - } - - #[cfg(not(feature = "wasm-deterministic"))] - pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { - let key = - LibSecp256k1PublicKey::from_secret_key_with_context(&privk.key, &ECMULT_GEN_CONTEXT); - Secp256k1PublicKey { - key, - compressed: privk.compress_public, - } - } - - #[cfg(not(feature = "wasm-deterministic"))] - /// recover message and signature to public key (will be compressed) - pub fn recover_to_pubkey( - msg: &[u8], - sig: &MessageSignature, - ) -> Result { - let secp256k1_sig = secp256k1_recover(msg, sig.as_bytes()) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - Secp256k1PublicKey::from_slice(&secp256k1_sig) - } -} - -impl Secp256k1PrivateKey { - #[cfg(feature = "rand")] - pub fn new() -> Secp256k1PrivateKey { - use rand::RngCore as _; - - let mut rng = rand::thread_rng(); - loop { - // keep trying to generate valid bytes - let mut random_32_bytes = [0u8; 32]; - rng.fill_bytes(&mut random_32_bytes); - let pk_res = LibSecp256k1PrivateKey::parse_slice(&random_32_bytes); - match pk_res { - Ok(pk) => { - return Secp256k1PrivateKey { - key: pk, - compress_public: true, - }; - } - Err(_) => { - continue; - } - } - } - } - - pub fn from_slice(data: &[u8]) -> Result { - if data.len() < 32 { - return Err("Invalid private key: shorter than 32 bytes"); - } - if data.len() > 33 { - return Err("Invalid private key: greater than 33 bytes"); - } - let compress_public = if data.len() == 33 { - // compressed byte tag? - if data[32] != 0x01 { - return Err("Invalid private key: invalid compressed byte marker"); - } - true - } else { - false - }; - - match LibSecp256k1PrivateKey::parse_slice(&data[0..32]) { - Ok(privkey_res) => Ok(Secp256k1PrivateKey { - key: privkey_res, - compress_public, - }), - Err(_e) => Err("Invalid private key: failed to load"), - } - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex private key")?; - Secp256k1PrivateKey::from_slice(&data[..]).map_err(|_e| "Invalid private key hex string") - } - - pub fn compress_public(&self) -> bool { - self.compress_public - } - - pub fn set_compress_public(&mut self, value: bool) { - self.compress_public = value; - } -} - -#[cfg(not(feature = "wasm-deterministic"))] -pub fn secp256k1_recover( - message_arr: &[u8], - serialized_signature: &[u8], -) -> Result<[u8; 33], LibSecp256k1Error> { - let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature[64] as u8)?; - let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature[..64])?; - let recovered_pub_key = libsecp256k1::recover(&message, &signature, &recovery_id)?; - Ok(recovered_pub_key.serialize_compressed()) -} - -#[cfg(not(feature = "wasm-deterministic"))] -pub fn secp256k1_verify( - message_arr: &[u8], - serialized_signature: &[u8], - pubkey_arr: &[u8], -) -> Result<(), LibSecp256k1Error> { - let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature[..64])?; // ignore 65th byte if present - let pubkey = LibSecp256k1PublicKey::parse_slice( - pubkey_arr, - Some(libsecp256k1::PublicKeyFormat::Compressed), - )?; - - let res = libsecp256k1::verify(&message, &signature, &pubkey); - if res { - Ok(()) - } else { - Err(LibSecp256k1Error::InvalidPublicKey) - } -} - -fn secp256k1_pubkey_serialize( - pubk: &LibSecp256k1PublicKey, - s: S, -) -> Result { - let key_hex = to_hex(&pubk.serialize().to_vec()); - s.serialize_str(&key_hex.as_str()) -} - -fn secp256k1_pubkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PublicKey::parse_slice(&key_bytes[..], None).map_err(de_Error::custom) -} - -fn secp256k1_privkey_serialize( - privk: &LibSecp256k1PrivateKey, - s: S, -) -> Result { - let key_hex = to_hex(&privk.serialize().to_vec()); - s.serialize_str(key_hex.as_str()) -} - -fn secp256k1_privkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PrivateKey::parse_slice(&key_bytes[..]).map_err(de_Error::custom) -} - -impl MessageSignature { - pub fn empty() -> MessageSignature { - // NOTE: this cannot be a valid signature - MessageSignature([0u8; 65]) - } - - #[cfg(test)] - // test method for generating place-holder data - pub fn from_raw(sig: &Vec) -> MessageSignature { - let mut buf = [0u8; 65]; - if sig.len() < 65 { - buf.copy_from_slice(&sig[..]); - } else { - buf.copy_from_slice(&sig[..65]); - } - MessageSignature(buf) - } - - pub fn from_secp256k1_recoverable( - sig: &LibSecp256k1Signature, - recid: LibSecp256k1RecoveryId, - ) -> MessageSignature { - let bytes = sig.serialize(); - let mut ret_bytes = [0u8; 65]; - let recovery_id_byte = recid.serialize(); // recovery ID will be 0, 1, 2, or 3 - ret_bytes[0] = recovery_id_byte; - ret_bytes[1..=64].copy_from_slice(&bytes[..64]); - MessageSignature(ret_bytes) - } - - pub fn to_secp256k1_recoverable( - &self, - ) -> Option<(LibSecp256k1Signature, LibSecp256k1RecoveryId)> { - let recovery_id = match LibSecp256k1RecoveryId::parse(self.0[0]) { - Ok(rid) => rid, - Err(_) => { - return None; - } - }; - let signature = LibSecp256k1Signature::parse_standard_slice(&self.0[1..65]).ok()?; - Some((signature, recovery_id)) - } -} - -impl PublicKey for Secp256k1PublicKey { - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - #[cfg(feature = "wasm-deterministic")] - fn verify(&self, _data_hash: &[u8], _sig: &MessageSignature) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(not(feature = "wasm-deterministic"))] - fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { - let pub_key = Secp256k1PublicKey::recover_to_pubkey(data_hash, sig)?; - Ok(self.eq(&pub_key)) - } -} - -impl PrivateKey for Secp256k1PrivateKey { - fn to_bytes(&self) -> Vec { - let mut bits = self.key.serialize().to_vec(); - if self.compress_public { - bits.push(0x01); - } - bits - } - - #[cfg(feature = "wasm-deterministic")] - fn sign(&self, _data_hash: &[u8]) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(not(feature = "wasm-deterministic"))] - fn sign(&self, data_hash: &[u8]) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash) - .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; - let (sig, recid) = libsecp256k1::sign(&message, &self.key); - let rec_sig = MessageSignature::from_secp256k1_recoverable(&sig, recid); - Ok(rec_sig) - } - - #[cfg(all(feature = "wasm-deterministic", any(test, feature = "testing")))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(all(any(test, feature = "testing"), not(feature = "wasm-deterministic")))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash) - .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; - let mut nonce = Scalar::default(); - let _ = nonce.set_b32(&noncedata); - - // we need this as the key raw data are private - let mut key = Scalar::default(); - let _ = key.set_b32(&self.key.serialize()); - - let (sigr, sigs, recid) = match ECMULT_GEN_CONTEXT.sign_raw(&key, &message.0, &nonce) { - Ok(result) => result, - Err(_) => return Err("unable to sign message"), - }; - - let recid = match LibSecp256k1RecoveryId::parse(recid) { - Ok(recid) => recid, - Err(_) => return Err("invalid recovery id"), - }; - - let (sig, recid) = (LibSecp256k1Signature { r: sigr, s: sigs }, recid); - let rec_sig = MessageSignature::from_secp256k1_recoverable(&sig, recid); - Ok(rec_sig) - } -} diff --git a/stacks-common/src/util/secp256r1.rs b/stacks-common/src/util/secp256r1.rs new file mode 100644 index 00000000000..f344eb3f57f --- /dev/null +++ b/stacks-common/src/util/secp256r1.rs @@ -0,0 +1,541 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use p256::ecdsa::signature::{Signer, Verifier}; +use p256::ecdsa::{ + Signature as P256Signature, SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey, +}; +use p256::elliptic_curve::generic_array::GenericArray; +use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use p256::{EncodedPoint, PublicKey as P256PublicKey, SecretKey as P256SecretKey}; +use thiserror::Error; + +use crate::util::hash::{hex_bytes, to_hex, Sha256Sum}; + +pub const MESSAGE_SIGNATURE_ENCODED_SIZE: u32 = 64; + +pub struct MessageSignature(pub [u8; 64]); +impl_array_newtype!(MessageSignature, u8, 64); +impl_array_hexstring_fmt!(MessageSignature); +impl_byte_array_newtype!(MessageSignature, u8, 64); +impl_byte_array_serde!(MessageSignature); + +#[derive(Debug, PartialEq, Eq, Clone, Error)] +pub enum Secp256r1Error { + #[error("Invalid key")] + InvalidKey, + #[error("Invalid signature")] + InvalidSignature, + #[error("Invalid message")] + InvalidMessage, + #[error("Invalid recovery ID")] + InvalidRecoveryId, + #[error("Signing failed")] + SigningFailed, +} + +/// A Secp256r1 public key +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Secp256r1PublicKey { + key: P256VerifyingKey, + compressed: bool, +} +impl_byte_array_serde!(Secp256r1PublicKey); + +/// A Secp256r1 private key +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Secp256r1PrivateKey { + key: P256SigningKey, + compress_public: bool, +} +impl_byte_array_serde!(Secp256r1PrivateKey); + +impl MessageSignature { + /// Creates an "empty" signature (all zeros). Note this is not a valid signature. + pub fn empty() -> MessageSignature { + // NOTE: this cannot be a valid signature + MessageSignature([0u8; 64]) + } + + /// Generates place-holder data (for testing purposes only) + #[cfg(any(test, feature = "testing"))] + pub fn from_raw(sig: &[u8]) -> MessageSignature { + let mut buf = [0u8; 64]; + if sig.len() < 64 { + buf[..sig.len()].copy_from_slice(sig); + } else { + buf.copy_from_slice(&sig[..64]); + } + MessageSignature(buf) + } + + /// Converts from a p256::ecdsa::Signature to our MessageSignature + pub fn from_p256_signature(sig: &P256Signature) -> MessageSignature { + let sig_bytes = sig.to_bytes(); + let mut ret_bytes = [0u8; 64]; + ret_bytes.copy_from_slice(&sig_bytes); + MessageSignature(ret_bytes) + } + + /// Converts to a p256::ecdsa::Signature + pub fn to_p256_signature(&self) -> Result { + P256Signature::from_slice(&self.0).map_err(|_| Secp256r1Error::InvalidSignature) + } +} + +impl Secp256r1PublicKey { + /// Generates a new random public key (for testing purposes only). + #[cfg(any(test, feature = "testing"))] + pub fn random() -> Secp256r1PublicKey { + Secp256r1PublicKey::from_private(&Secp256r1PrivateKey::random()) + } + + /// Creates a Secp256r1PublicKey from a hex string representation. + pub fn from_hex(hex_string: &str) -> Result { + let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex public key")?; + Secp256r1PublicKey::from_slice(&data[..]).map_err(|_e| "Invalid public key hex string") + } + + /// Creates a Secp256r1PublicKey from a byte slice. + pub fn from_slice(data: &[u8]) -> Result { + let encoded_point = EncodedPoint::from_bytes(data) + .map_err(|_| "Invalid public key: failed to parse encoded point")?; + + let public_key = + Option::::from(P256PublicKey::from_encoded_point(&encoded_point)) + .ok_or("Invalid public key: failed to decode point")?; + + let verifying_key = P256VerifyingKey::from(public_key); + + Ok(Secp256r1PublicKey { + key: verifying_key, + compressed: data.len() == 33, // 33 bytes = compressed, 65 bytes = uncompressed + }) + } + + /// Creates a Secp256r1PublicKey from a Secp256r1PrivateKey. + pub fn from_private(privk: &Secp256r1PrivateKey) -> Secp256r1PublicKey { + let verifying_key = privk.key.verifying_key(); + Secp256r1PublicKey { + key: *verifying_key, + compressed: privk.compress_public, + } + } + + /// Converts the public key to a hex string representation. + pub fn to_hex(&self) -> String { + to_hex(&self.to_bytes()) + } + + pub fn to_bytes(&self) -> Vec { + let public_key = P256PublicKey::from(&self.key); + let encoded_point = public_key.to_encoded_point(self.compressed); + encoded_point.as_bytes().to_vec() + } + + /// Converts the public key to a compressed byte representation. + pub fn to_bytes_compressed(&self) -> Vec { + let public_key = P256PublicKey::from(&self.key); + let encoded_point = public_key.to_encoded_point(true); // true = compressed + encoded_point.as_bytes().to_vec() + } + + /// Returns whether the public key should be in compressed format when serialized. + pub fn compressed(&self) -> bool { + self.compressed + } + + /// Sets whether the public key should be in compressed format when serialized. + pub fn set_compressed(&mut self, value: bool) { + self.compressed = value; + } + + /// Verify a signature against a message hash. + /// Returns Ok(()) if the signature is valid, or an error otherwise. + pub fn verify_digest( + &self, + msg_hash: &[u8], + sig: &MessageSignature, + ) -> Result<(), Secp256r1Error> { + if msg_hash.len() != 32 { + return Err(Secp256r1Error::InvalidMessage); + } + + let p256_sig = sig + .to_p256_signature() + .map_err(|_| Secp256r1Error::InvalidSignature)?; + + // Verify the signature + self.key + .verify(msg_hash, &p256_sig) + .map_err(|_| Secp256r1Error::InvalidSignature) + } +} + +#[cfg(any(test, feature = "testing"))] +impl Default for Secp256r1PublicKey { + fn default() -> Self { + Self::random() + } +} + +impl Secp256r1PrivateKey { + /// Generates a new random private key. + #[cfg(feature = "rand")] + pub fn random() -> Secp256r1PrivateKey { + let secret_key = P256SecretKey::random(&mut rand::thread_rng()); + let signing_key = P256SigningKey::from(secret_key); + Secp256r1PrivateKey { + key: signing_key, + compress_public: true, + } + } + + /// Creates a Secp256r1PrivateKey from seed bytes by repeatedly + /// SHA256 hashing the seed bytes until a private key is found. + /// + /// If `seed` is a valid private key, it will be returned without hashing. + /// The returned private key's compress_public flag will be `true`. + pub fn from_seed(seed: &[u8]) -> Secp256r1PrivateKey { + let mut re_hashed_seed = Vec::from(seed); + loop { + if let Ok(mut sk) = Secp256r1PrivateKey::from_slice(&re_hashed_seed[..]) { + sk.set_compress_public(true); + return sk; + } else { + re_hashed_seed = Sha256Sum::from_data(&re_hashed_seed[..]) + .as_bytes() + .to_vec() + } + } + } + + /// Creates a Secp256r1PrivateKey from a hex string representation. + pub fn from_hex(hex_string: &str) -> Result { + let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex private key")?; + Secp256r1PrivateKey::from_slice(&data[..]).map_err(|_e| "Invalid private key hex string") + } + + /// Creates a Secp256r1PrivateKey from a byte slice. + pub fn from_slice(data: &[u8]) -> Result { + if data.len() < 32 { + return Err("Invalid private key: shorter than 32 bytes"); + } + if data.len() > 33 { + return Err("Invalid private key: greater than 33 bytes"); + } + let compress_public = if data.len() == 33 { + // compressed byte tag? + if data[32] != 0x01 { + return Err("Invalid private key: invalid compressed byte marker"); + } + true + } else { + false + }; + + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(&data[0..32]); + + let secret_key = P256SecretKey::from_bytes(&GenericArray::from(key_bytes)) + .map_err(|_| "Invalid private key: failed to load")?; + let signing_key = P256SigningKey::from(secret_key); + + Ok(Secp256r1PrivateKey { + key: signing_key, + compress_public, + }) + } + + /// Returns whether the corresponding public key should be in compressed format when + /// serialized. + pub fn compress_public(&self) -> bool { + self.compress_public + } + + /// Sets whether the corresponding public key should be in compressed format when serialized. + pub fn set_compress_public(&mut self, value: bool) { + self.compress_public = value; + } + + /// Converts the private key to a hex string representation. + pub fn to_hex(&self) -> String { + let mut bytes = self.key.to_bytes().to_vec(); + if self.compress_public { + bytes.push(1); + } + to_hex(&bytes) + } + + /// Converts the private key to a byte vector representation. + pub fn to_bytes(&self) -> Vec { + let mut bits = self.key.to_bytes().to_vec(); + if self.compress_public { + bits.push(0x01); + } + bits + } + + /// Sign a message hash, returning the signature. + /// The message must be a 32-byte hash. + pub fn sign(&self, data_hash: &[u8]) -> Result { + if data_hash.len() != 32 { + return Err("Invalid message: must be a 32-byte hash"); + } + + let signature: P256Signature = self.key.sign(data_hash); + Ok(MessageSignature::from_p256_signature(&signature)) + } +} + +/// Verify a secp256r1 signature. +/// The message must be a 32-byte hash. +/// The signature must be a 64-byte compact signature +pub fn secp256r1_verify( + message_arr: &[u8], + signature_arr: &[u8], + pubkey_arr: &[u8], +) -> Result<(), Secp256r1Error> { + let msg: &[u8; 32] = message_arr + .try_into() + .map_err(|_| Secp256r1Error::InvalidMessage)?; + let sig_bytes: &[u8; 64] = signature_arr + .try_into() + .map_err(|_| Secp256r1Error::InvalidSignature)?; + + let pk = Secp256r1PublicKey::from_slice(pubkey_arr).map_err(|_| Secp256r1Error::InvalidKey)?; + let sig = MessageSignature::from_bytes(sig_bytes).ok_or(Secp256r1Error::InvalidSignature)?; + pk.verify_digest(msg, &sig) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_privkey_parse_serialize_compressed() { + let mut t1 = Secp256r1PrivateKey::random(); + t1.set_compress_public(true); + let h_comp = t1.to_hex(); + let json_comp = serde_json::to_string(&t1).unwrap(); + assert_eq!(json_comp, format!("\"{h_comp}\"")); + let deser_comp: Secp256r1PrivateKey = serde_json::from_str(&json_comp).unwrap(); + assert_eq!(deser_comp.to_hex(), h_comp); + assert!(deser_comp.compress_public()); + + t1.set_compress_public(false); + let h_uncomp = t1.to_hex(); + let json_uncomp = serde_json::to_string(&t1).unwrap(); + assert_eq!(json_uncomp, format!("\"{h_uncomp}\"")); + let deser_uncomp: Secp256r1PrivateKey = serde_json::from_str(&json_uncomp).unwrap(); + assert_eq!(deser_uncomp.to_hex(), h_uncomp); + assert!(!deser_uncomp.compress_public()); + + assert!(h_comp != h_uncomp); + assert_eq!(h_comp.len(), 66); + assert_eq!(h_uncomp.len(), 64); + + let (uncomp, comp_value) = h_comp.split_at(64); + assert_eq!(comp_value, "01"); + assert_eq!(uncomp, &h_uncomp); + + assert!(Secp256r1PrivateKey::from_hex(&h_comp) + .unwrap() + .compress_public()); + assert!(!Secp256r1PrivateKey::from_hex(&h_uncomp) + .unwrap() + .compress_public()); + + assert_eq!(Secp256r1PrivateKey::from_hex(&h_uncomp), Ok(t1.clone())); + + t1.set_compress_public(true); + + assert_eq!(Secp256r1PrivateKey::from_hex(&h_comp), Ok(t1)); + } + + #[test] + fn test_pubkey_parse_serialize_compressed() { + let privk = Secp256r1PrivateKey::random(); + let mut pubk = Secp256r1PublicKey::from_private(&privk); + + pubk.set_compressed(true); + let h_comp = pubk.to_hex(); + let json_comp = serde_json::to_string(&pubk).unwrap(); + assert_eq!(json_comp, format!("\"{}\"", h_comp)); + let deser_comp: Secp256r1PublicKey = serde_json::from_str(&json_comp).unwrap(); + assert_eq!(deser_comp.to_hex(), h_comp); + assert!(deser_comp.compressed()); + + pubk.set_compressed(false); + let h_uncomp = pubk.to_hex(); + let json_uncomp = serde_json::to_string(&pubk).unwrap(); + assert_eq!(json_uncomp, format!("\"{}\"", h_uncomp)); + let deser_uncomp: Secp256r1PublicKey = serde_json::from_str(&json_uncomp).unwrap(); + assert_eq!(deser_uncomp.to_hex(), h_uncomp); + assert!(!deser_uncomp.compressed()); + + assert!(h_comp != h_uncomp); + assert_eq!(h_comp.len(), 66); + assert_eq!(h_uncomp.len(), 130); + + assert!(Secp256r1PublicKey::from_hex(&h_comp).unwrap().compressed()); + assert!(!Secp256r1PublicKey::from_hex(&h_uncomp) + .unwrap() + .compressed()); + + assert_eq!(Secp256r1PublicKey::from_hex(&h_uncomp), Ok(pubk.clone())); + + pubk.set_compressed(true); + + assert_eq!(Secp256r1PublicKey::from_hex(&h_comp), Ok(pubk)); + } + + #[test] + fn test_from_seed() { + let sk = Secp256r1PrivateKey::from_seed(&[2; 32]); + let pubk = Secp256r1PublicKey::from_private(&sk); + + // Test that from_seed is deterministic + let sk2 = Secp256r1PrivateKey::from_seed(&[2; 32]); + let pubk2 = Secp256r1PublicKey::from_private(&sk2); + + assert_eq!(sk.to_hex(), sk2.to_hex()); + assert_eq!(pubk.to_hex(), pubk2.to_hex()); + } + + #[test] + fn test_roundtrip_sign_verify() { + let privk = Secp256r1PrivateKey::random(); + let pubk = Secp256r1PublicKey::from_private(&privk); + + let msg = b"hello world"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + let sig = privk.sign(&msg_hash).unwrap(); + pubk.verify_digest(&msg_hash, &sig) + .expect("invalid signature"); + } + + #[test] + fn test_verify_with_different_key() { + let privk1 = Secp256r1PrivateKey::random(); + let privk2 = Secp256r1PrivateKey::random(); + let pubk2 = Secp256r1PublicKey::from_private(&privk2); + + let msg = b"hello world"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + let sig = privk1.sign(&msg_hash).unwrap(); + let e = pubk2 + .verify_digest(&msg_hash, &sig) + .expect_err("expected an error"); + assert_eq!(e, Secp256r1Error::InvalidSignature); + } + + #[test] + fn test_public_key_compression() { + let privk = Secp256r1PrivateKey::random(); + let mut pubk = Secp256r1PublicKey::from_private(&privk); + + pubk.set_compressed(true); + let compressed_bytes = pubk.to_bytes(); + assert_eq!(compressed_bytes.len(), 33); + + pubk.set_compressed(false); + let uncompressed_bytes = pubk.to_bytes(); + assert_eq!(uncompressed_bytes.len(), 65); + + // Both should parse back to the same key + let pubk_from_compressed = Secp256r1PublicKey::from_slice(&compressed_bytes).unwrap(); + let pubk_from_uncompressed = Secp256r1PublicKey::from_slice(&uncompressed_bytes).unwrap(); + + assert_eq!(pubk_from_compressed.key, pubk_from_uncompressed.key); + } + + #[test] + fn test_high_s_signature() { + use crate::util::hash::Sha256Sum; + + let privk = Secp256r1PrivateKey::random(); + let pubk = Secp256r1PublicKey::from_private(&privk); + + let msg = b"stacks secp256r1 high-s test____"; + let msg_hash = Sha256Sum::from_data(msg).as_bytes().to_vec(); + + // Sign the message + let sig = privk.sign(&msg_hash).unwrap(); + let pubkey_bytes = pubk.to_bytes(); + + // Get the underlying P256Signature to work with r,s components + let original_sig = P256Signature::from_slice(&sig.0).unwrap(); + + // Always get the low-s version first + let low_sig = if let Some(normalized) = original_sig.normalize_s() { + normalized // Original was high-s, use the normalized (low-s) version + } else { + original_sig // Original was already low-s + }; + + // Now create high-s version from the low-s signature + let (r, s) = (low_sig.r(), low_sig.s()); + let high_sig = { + // Make high-s by negating s (s' = -s mod n) + let s_hi = -(*s); + P256Signature::from_scalars(*r, s_hi).expect("valid (r, -s)") + }; + + let low_bytes = low_sig.to_bytes(); + let high_bytes = high_sig.to_bytes(); + + // Verify our assumptions about which is which + let low_is_low_s = low_sig.normalize_s().is_none(); + let high_is_high_s = high_sig.normalize_s().is_some(); + + assert!(low_is_low_s, "Low signature should be low-s"); + assert!(high_is_high_s, "High signature should be high-s"); + + // Low-s signature should pass verification + let low_result = secp256r1_verify(&msg_hash, &low_bytes, &pubkey_bytes); + assert!( + low_result.is_ok(), + "Low-s signature should pass verification" + ); + + // High-s signature should pass verification + let high_result = secp256r1_verify(&msg_hash, &high_bytes, &pubkey_bytes); + assert!( + high_result.is_ok(), + "High-s signature should pass verification" + ); + + // Test normalization: high-s should pass when normalized to low-s + if let Some(normalized_sig) = high_sig.normalize_s() { + let normalized_bytes = normalized_sig.to_bytes(); + let normalized_result = secp256r1_verify(&msg_hash, &normalized_bytes, &pubkey_bytes); + assert!( + normalized_result.is_ok(), + "Normalized (low-s) signature should pass verification" + ); + + // The normalized signature should be the same as our low signature + assert_eq!( + normalized_bytes, low_bytes, + "Normalized signature should match our low signature" + ); + } else { + panic!("High-s signature should normalize to low-s"); + } + } +} diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml index 6f7f00f57ce..51820947f7e 100644 --- a/stacks-signer/Cargo.toml +++ b/stacks-signer/Cargo.toml @@ -56,10 +56,6 @@ stdext = "0.3.1" version = "1.0" features = ["arbitrary_precision", "unbounded_depth"] -[dependencies.secp256k1] -version = "0.24.3" -features = ["serde", "recovery"] - [features] default = [] monitoring_prom = ["prometheus", "tiny_http"] diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index b0e0cc03bd1..e02f46afd1c 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -1995,7 +1995,6 @@ pub mod tests { TransactionVersion, }; use clarity::types::chainstate::{StacksBlockId, StacksPrivateKey, StacksPublicKey}; - use clarity::types::PrivateKey; use clarity::util::hash::Hash160; use clarity::util::secp256k1::MessageSignature; use libsigner::v0::messages::{StateMachineUpdateContent, StateMachineUpdateMinerState}; @@ -2468,25 +2467,10 @@ pub mod tests { let address1 = StacksAddress::p2pkh(false, &public_key1); let address2 = StacksAddress::p2pkh(false, &public_key2); - let nonce1 = [0x11u8; 32]; - let signature1 = private_key1 - .sign_with_noncedata(&block_id.0, &nonce1) - .unwrap(); - - let nonce2 = [0x22u8; 32]; - let signature2 = private_key1 - .sign_with_noncedata(&block_id.0, &nonce2) - .unwrap(); - - let nonce3 = [0x33u8; 32]; - let signature3 = private_key1 - .sign_with_noncedata(&block_id.0, &nonce3) - .unwrap(); - - let nonce4 = [0x44u8; 32]; - let signature4 = private_key2 - .sign_with_noncedata(&block_id.0, &nonce4) - .unwrap(); + let signature1 = MessageSignature::from_raw(&[0x11]); + let signature2 = MessageSignature::from_raw(&[0x22]); + let signature3 = MessageSignature::from_raw(&[0x33]); + let signature4 = MessageSignature::from_raw(&[0x44]); assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![]); diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index 6d2f85dfd04..10e94f4b470 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -59,10 +59,6 @@ winapi = { version = "0.3", features = ["fileapi", "processenv", "winnt"] } version = "1.0" features = ["arbitrary_precision", "unbounded_depth"] -[dependencies.secp256k1] -version = "0.24.3" -features = ["serde", "recovery"] - [dependencies.ed25519-dalek] workspace = true diff --git a/stackslib/fuzz/Cargo.lock b/stackslib/fuzz/Cargo.lock index 86e0347d47a..e17fd2f7514 100644 --- a/stackslib/fuzz/Cargo.lock +++ b/stackslib/fuzz/Cargo.lock @@ -44,12 +44,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "autocfg" version = "1.5.0" @@ -57,10 +51,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base64" -version = "0.22.1" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" @@ -74,15 +74,6 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -166,6 +157,12 @@ dependencies = [ "stacks-common", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -182,10 +179,16 @@ dependencies = [ ] [[package]] -name = "crunchy" -version = "0.2.4" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" @@ -197,16 +200,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -216,7 +209,7 @@ dependencies = [ "cfg-if 1.0.3", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", "rustc_version", "serde", @@ -235,21 +228,22 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.5.3" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "powerfmt", + "const-oid", + "zeroize", ] [[package]] -name = "digest" -version = "0.9.0" +name = "deranged" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ - "generic-array", + "powerfmt", ] [[package]] @@ -258,8 +252,10 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -273,6 +269,21 @@ dependencies = [ "syn", ] +[[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", + "serdect", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -290,8 +301,28 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "sha2 0.10.9", + "sha2", + "subtle", +] + +[[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", + "pkcs8", + "rand_core", + "sec1", + "serdect", "subtle", + "zeroize", ] [[package]] @@ -312,6 +343,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -363,6 +404,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -388,6 +430,17 @@ dependencies = [ "wasi 0.14.7+wasi-0.2.4", ] +[[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 = "hashbrown" version = "0.14.5" @@ -426,23 +479,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - -[[package]] -name = "hmac-drbg" -version = "0.3.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.9.0", - "generic-array", - "hmac", + "digest", ] [[package]] @@ -631,6 +672,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.3", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", +] + [[package]] name = "keccak" version = "0.1.5" @@ -672,55 +727,6 @@ dependencies = [ "cc", ] -[[package]] -name = "libsecp256k1" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" -dependencies = [ - "arrayref", - "base64", - "digest 0.9.0", - "hmac-drbg", - "lazy_static", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand", - "serde", - "sha2 0.9.9", - "typenum", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "libsqlite3-sys" version = "0.28.0" @@ -737,9 +743,8 @@ name = "libstackerdb" version = "0.0.1" dependencies = [ "clarity", - "secp256k1", "serde", - "sha2 0.10.9", + "sha2", "stacks-common", ] @@ -847,10 +852,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2", +] [[package]] name = "percent-encoding" @@ -858,6 +870,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -897,6 +919,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -989,13 +1021,23 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ripemd" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1035,22 +1077,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "secp256k1" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" -dependencies = [ - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.6.1" +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "cc", + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", ] [[package]] @@ -1114,16 +1152,13 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.9.9" +name = "serdect" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.3", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "base16ct", + "serde", ] [[package]] @@ -1134,7 +1169,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if 1.0.3", "cpufeatures", - "digest 0.10.7", + "digest", "sha2-asm", ] @@ -1153,7 +1188,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -1168,6 +1203,10 @@ name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] [[package]] name = "siphasher" @@ -1207,6 +1246,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1234,20 +1283,21 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "hashbrown 0.15.5", + "k256", "lazy_static", - "libsecp256k1", "nix", + "p256", "rand", "ripemd", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", - "sha2 0.10.9", + "sha2", "sha3", "slog", "slog-term", + "thiserror", "toml", "winapi 0.3.9", ] @@ -1270,11 +1320,10 @@ dependencies = [ "regex", "ripemd", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", - "sha2 0.10.9", + "sha2", "siphasher", "slog", "stacks-common", @@ -1330,6 +1379,26 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1775,6 +1844,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/stackslib/src/chainstate/stacks/block.rs b/stackslib/src/chainstate/stacks/block.rs index 3a21ebb0867..9e4343237e8 100644 --- a/stackslib/src/chainstate/stacks/block.rs +++ b/stackslib/src/chainstate/stacks/block.rs @@ -660,6 +660,8 @@ impl StacksMessageCodec for StacksMicroblockHeader { let signature: MessageSignature = read_next(fd)?; // signature must be well-formed + // in tests, we sometimes use invalid signatures + #[cfg(not(any(test, feature = "testing")))] let _ = signature .to_secp256k1_recoverable() .ok_or(codec_error::DeserializeError( diff --git a/stackslib/src/chainstate/stacks/boot/costs-4.clar b/stackslib/src/chainstate/stacks/boot/costs-4.clar index f83afdcdf4a..a6e085d470d 100644 --- a/stackslib/src/chainstate/stacks/boot/costs-4.clar +++ b/stackslib/src/chainstate/stacks/boot/costs-4.clar @@ -669,3 +669,6 @@ (define-read-only (cost_as_contract_safe (n uint)) (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark + +(define-read-only (cost_secp256r1verify (n uint)) + (runtime u1)) ;; TODO: needs criterion benchmark diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap index 65ae5f95fd2..fe3789d26c9 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap @@ -62,7 +62,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "7e9b86e5b0ff545908784c1c674d1354228ce2884395ced13f7c9e4eaa7ecfd0", + marf_hash: "30fa36b4dc9398e9792996eb65440cb6c7f05212566782502c2677f5c7793e6f", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__chainstate_error_expression_stack_depth_too_deep.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__chainstate_error_expression_stack_depth_too_deep.snap index e1f44a75010..704e5073bc0 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__chainstate_error_expression_stack_depth_too_deep.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__chainstate_error_expression_stack_depth_too_deep.snap @@ -6,8 +6,8 @@ expression: result Failure("Invalid Stacks block 7e31cbb71c79284d65fcf9ed65e7263e6320f267f8b29eb31aca3a8fcb65302b: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), Failure("Invalid Stacks block 1204d27abf8c5274ca561a1f9d14c7d64c3f802ff2b78a164d7d29eb8808d547: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), Failure("Invalid Stacks block 435d0c567d8f970c7fde521b4b22e270e78ffbb8c8b09e33bbc6d5b4e7d0257f: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block 76cc5894a268fa256bb03bfd5e378de73db2a9ab1c88b9a0c0dca80c41782137: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block e9b506ca1ce75c6ee393200d7ab89742e0dd74f64b1b3363fd98411d163251e7: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block c1dfc3fad9244f636197bd062b88a8aee581a0df0f2632f7f4639ada153e88d6: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block 072ea79f6c9908c904a6c90881324c52ba67f7a27887e932571ac26e2ad03d19: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block c391035a8457de4600bb9d48e2d4fd7e54a553f4143758db09740f0fc61374df: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block 01082196750dcd7f830d841338a95a1400a96f35e91135d3992db57fd907dba7: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block 1044b36b10f92b9ee71a0424d5e6c1e52d3059a5d9f512499b1b6ecc54ce85a3: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block a3eaa78beaae7d208893cadb3a11541b6d1f85c8473ba39e1886ed041dce13a6: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__successfully_deploy_and_call.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__successfully_deploy_and_call.snap index 8d9eddf5f4c..ca48c4b8817 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__successfully_deploy_and_call.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__successfully_deploy_and_call.snap @@ -508,7 +508,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "c0d6892c54926e704f2b79f5bfcea311b6ecbfd1709de05d8ed817f54f392e1c", + marf_hash: "3539942bb7564287a51bd097fe7cdfb9eaa53ede9d9a663020ea78fe121de30a", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -536,7 +536,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "bc08a038f2c9819baeb15f306063c351b2ad4d2b6297de98e5019c0655ac29d1", + marf_hash: "68ca796dc9f88eeb00499b4a1ad5c8e7228bed0ea57c1af1632f835a13b0994f", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -564,7 +564,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "deda548b30bd858b6b42b638258ea0a45919972b339604544090af42f7c4330b", + marf_hash: "780edfe9dd24914ce2758ba0c4ffb84db6a22ae7ad6ddf78d86bf74da9dbf12c", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -592,7 +592,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "1f8603c7d580008f986774bc4a624d429d5b45b4a4385c14656a5398bf8a56ed", + marf_hash: "ee5328e8f72017cc9a896280a5d9c6efb22ee0e1f5daec777ef8c96125e9f58d", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -620,7 +620,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "59df1ff6ea26ae936061b8565b962dfbd0d9420efedbf9c9aefb43365e4da789", + marf_hash: "14473254bf7f8d4ec1fa9d4796bc3d965c93156c66228505ff537691e99d7197", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -648,7 +648,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "fb5d51ffb295367d9dcd567775618bd54b21c56a9503c2678fac49f53d5e23fa", + marf_hash: "bf701aa24dd21b45da9ebb1068ffb0ce685e45feab3587c00fa10df9982d9ce3", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -676,7 +676,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "505471f1004e5ab38ef24a05b22422b8d66c9313a85c1d9c3470204d7e38f422", + marf_hash: "3552edb6ace6b7219d9b7c8991cafbce2167e9f7a66691ffa15dc0e01f2caee1", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -704,7 +704,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "5260eec1120a1caeaceb27842cb4e4a235cbb0791fb67682499bb3796d07c31a", + marf_hash: "455eaf60321e701681d74b802768ef1996825e094ffde5bcb39c9bc7b358ba11", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -732,7 +732,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "105587537589f0bdad491c5cbcdfcffea475176a064c01ffd7f61a1eedd15af5", + marf_hash: "246828fd4ab7cbbdd36c5559b06715662fd29b718af016d91a23101985b5f918", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -760,7 +760,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "861d1186952ddb0e242bfbe997ea9860cc6739880b47a0c268d7211d54942740", + marf_hash: "ec7fa4cac0b8dfe2e9febb574823dfde63b949b20a539543accadbb57db80e5c", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -788,7 +788,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "235447452a8921fe9bbf8999194cd001e0ded05bfcbf220b5408be28a4b505f8", + marf_hash: "d693bef23c7cf10554770310e748f36f351f0b584b95e4d153308a2748ea88e0", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -816,7 +816,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "74ea86662425f6fe76a58bb677e85aaab12ca27b2ec9a2e0c69bbb401275a859", + marf_hash: "59966cbe73d2f8af9864d282abdb4b9667268d2ecf71b31d6592463e0a3ef779", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -844,7 +844,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "795d447db68ddb26b9fd4d091f9e0643c48d44cf34b4c584b916ffe4281c5041", + marf_hash: "0f5e1f6d0abf4f847355fc43e0dd91b17bfee42d5959a5012a1aeda7f6a489e7", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -872,7 +872,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "228ad67aa6b7422fc7c8dbbc31c3e8e3042fe8c6bbd6d6a7407788b6d6902388", + marf_hash: "e63a5434198229d3d4d8fe673cc1d4775c4703cd388e23ab35d1bde65c932824", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -900,7 +900,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "31234b0b5cbda935741846fc6aee0138858ee589a8610587485fe2ac6ccbde3b", + marf_hash: "43cf4533cf166af9599710ff0474071c284b851a9ff9df1f835a0fe17a2e0e04", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -928,7 +928,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "5254245eb17bc1de6da56501a52b9a5f5fb8fd19c40eac285778a46eddc864c8", + marf_hash: "bfc87bcbef2f3e2f013bccae348fecedf36aa3d26329a36541d2d8824a05688d", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( @@ -956,7 +956,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "9a7ed0c58f9c0611f61c78a71741c7f0256402e5493843e6b1e327a8e7f2157c", + marf_hash: "73f00b7098abc4d2b3163d4efb3317af8909af96937149a8733bf48b98252562", evaluated_epoch: Epoch33, transactions: [ ExpectedTransactionOutput( diff --git a/stackslib/src/clarity_vm/tests/costs.rs b/stackslib/src/clarity_vm/tests/costs.rs index 181ceef16d8..62cf5d616ea 100644 --- a/stackslib/src/clarity_vm/tests/costs.rs +++ b/stackslib/src/clarity_vm/tests/costs.rs @@ -187,6 +187,7 @@ pub fn get_simple_test(function: &NativeFunctions) -> Option<&'static str> { ToAscii => "(to-ascii? 65)", RestrictAssets => "(restrict-assets? tx-sender () (+ u1 u2))", AsContractSafe => "(as-contract? () (+ u1 u2))", + Secp256r1Verify => "(secp256r1-verify 0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8 0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd310eac59293c23e6d6aa8b0c5d9e4e48fa4c4fdf1ace2ba618dc0263b5e90a0903 0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e)", // These expressions are not usable in this context, since they are // only allowed within `restrict-assets?` or `as-contract?` AllowanceWithStx