diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a49caf6689..4020c228f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,16 @@ 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` +- 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..183008b580b 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", @@ -3307,7 +3464,6 @@ dependencies = [ "rand_core 0.6.4", "reqwest", "rusqlite", - "secp256k1", "serde", "serde_json", "slog", @@ -3381,7 +3537,6 @@ dependencies = [ "rstest", "rstest_reuse", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index cfacd5b9ee2..6c2bcea9ac8 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -185,8 +185,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 21b2bbd8f7b..51e2962389c 100644 --- a/clarity/src/vm/analysis/read_only_checker/mod.rs +++ b/clarity/src/vm/analysis/read_only_checker/mod.rs @@ -285,17 +285,17 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater | Modulo | Power | Sqrti | Log2 | BitwiseXor | And | Or | Not | Hash160 | Sha256 | Keccak256 | Equals | If | Sha512 | Sha512Trunc256 | Secp256k1Recover - | Secp256k1Verify | ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet - | UnwrapErrRet | IsOkay | IsNone | Asserts | Unwrap | UnwrapErr | Match | IsErr - | IsSome | TryRet | ToUInt | ToInt | BuffToIntLe | BuffToUIntLe | BuffToIntBe - | BuffToUIntBe | IntToAscii | IntToUtf8 | StringToInt | StringToUInt | IsStandard - | ToConsensusBuff | PrincipalDestruct | PrincipalConstruct | Append | Concat - | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo - | GetStacksBlockInfo | GetTenureInfo | TupleGet | TupleMerge | Len | Print - | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount | GetTokenBalance - | GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice | ReplaceAt - | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift | BitwiseXor2 - | ElementAtAlias | IndexOfAlias | ContractHash | ToAscii => { + | Secp256k1Verify | Secp256r1Verify | ConsSome | ConsOkay | ConsError | DefaultTo + | UnwrapRet | UnwrapErrRet | IsOkay | IsNone | Asserts | Unwrap | UnwrapErr | Match + | IsErr | IsSome | TryRet | ToUInt | ToInt | BuffToIntLe | BuffToUIntLe + | BuffToIntBe | BuffToUIntBe | IntToAscii | IntToUtf8 | StringToInt | StringToUInt + | IsStandard | ToConsensusBuff | PrincipalDestruct | PrincipalConstruct | Append + | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo + | GetBurnBlockInfo | GetStacksBlockInfo | GetTenureInfo | TupleGet | TupleMerge + | Len | Print | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount + | GetTokenBalance | GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice + | ReplaceAt | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift + | BitwiseXor2 | ElementAtAlias | IndexOfAlias | ContractHash | ToAscii => { // Check all arguments. self.check_each_expression_is_read_only(args) } 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 4cd38adb1ce..4b408a8a006 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 @@ -782,7 +782,8 @@ impl TypedNativeFunction { | StringToUInt | IntToAscii | IntToUtf8 | GetBurnBlockInfo | StxTransferMemo | StxGetAccount | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift | BitwiseXor2 | Slice | ToConsensusBuff | FromConsensusBuff - | ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii => { + | ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii + | 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 397d4eb3235..35f5171293b 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 @@ -725,6 +725,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], @@ -1210,6 +1222,7 @@ impl TypedNativeFunction { CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into()) })?, ))), + 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 413ae80f29d..e9ade643637 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 @@ -41,6 +41,17 @@ use crate::vm::{execute_v2, ClarityName, ClarityVersion}; mod assets; pub mod contracts; +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( @@ -3855,3 +3866,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 6abbaec5556..98593ea73d9 100644 --- a/clarity/src/vm/costs/cost_functions.rs +++ b/clarity/src/vm/costs/cost_functions.rs @@ -159,6 +159,7 @@ define_named_enum!(ClarityCostFunction { BitwiseRShift("cost_bitwise_right_shift"), ContractHash("cost_contract_hash"), ToAscii("cost_to_ascii"), + Secp256r1verify("cost_secp256r1verify"), Unimplemented("cost_unimplemented"), }); @@ -330,6 +331,7 @@ pub trait CostValues { fn cost_bitwise_right_shift(n: u64) -> InterpreterResult; fn cost_contract_hash(n: u64) -> InterpreterResult; fn cost_to_ascii(n: u64) -> InterpreterResult; + fn cost_secp256r1verify(n: u64) -> InterpreterResult; } impl ClarityCostFunction { @@ -484,6 +486,7 @@ impl ClarityCostFunction { ClarityCostFunction::BitwiseRShift => C::cost_bitwise_right_shift(n), ClarityCostFunction::ContractHash => C::cost_contract_hash(n), ClarityCostFunction::ToAscii => C::cost_to_ascii(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 1e400f56bd5..a869958891a 100644 --- a/clarity/src/vm/costs/costs_1.rs +++ b/clarity/src/vm/costs/costs_1.rs @@ -753,4 +753,8 @@ impl CostValues for Costs1 { fn cost_to_ascii(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 451008bd1b9..da5a3a16505 100644 --- a/clarity/src/vm/costs/costs_2.rs +++ b/clarity/src/vm/costs/costs_2.rs @@ -753,4 +753,8 @@ impl CostValues for Costs2 { fn cost_to_ascii(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 647bafedb96..e090cb2d0e9 100644 --- a/clarity/src/vm/costs/costs_2_testnet.rs +++ b/clarity/src/vm/costs/costs_2_testnet.rs @@ -753,4 +753,8 @@ impl CostValues for Costs2Testnet { fn cost_to_ascii(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 b195303510c..86d52c33b39 100644 --- a/clarity/src/vm/costs/costs_3.rs +++ b/clarity/src/vm/costs/costs_3.rs @@ -771,4 +771,8 @@ impl CostValues for Costs3 { fn cost_to_ascii(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 d1c92732d98..c1b087ab74a 100644 --- a/clarity/src/vm/costs/costs_4.rs +++ b/clarity/src/vm/costs/costs_4.rs @@ -461,4 +461,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 8ab926834ab..7d66be290a1 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -1400,6 +1400,21 @@ 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 the `sha256` of the message and `signature` is the raw 64-byte signature.", + 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}", @@ -2680,6 +2695,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { BitwiseRShift => make_for_simple_native(&BITWISE_RIGHT_SHIFT_API, function, name), ContractHash => make_for_simple_native(&CONTRACT_HASH, function, name), ToAscii => make_for_special(&TO_ASCII, 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..0821030f123 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 f458c282e6c..dbcd882a204 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -193,6 +193,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { GetTenureInfo("get-tenure-info?", ClarityVersion::Clarity3, None), ContractHash("contract-hash?", ClarityVersion::Clarity4, None), ToAscii("to-ascii?", ClarityVersion::Clarity4, None), + Secp256r1Verify("secp256r1-verify", ClarityVersion::Clarity4, None), }); /// @@ -565,6 +566,9 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option SpecialFunction("special_contract_hash", &database::special_contract_hash) } ToAscii => SpecialFunction("special_to_ascii", &conversions::special_to_ascii), + 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..a5fd6dcea29 --- /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_false() { + let message = "0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8"; + let signature = "0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd31f153a6d5c3dc192a5574f3a261b1b70570971b92d8ebf86c17b7670d13591c4e"; + let pubkey = "0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e"; + + let program = format!("(secp256r1-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_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 a10aa7b1282..cee01679f0f 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; mod principals; 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..fd37ce0fd12 100644 --- a/stacks-common/Cargo.toml +++ b/stacks-common/Cargo.toml @@ -43,6 +43,10 @@ sha3 = { version = "0.10.1", default-features = false } slog = { workspace = true } slog-term = { version = "2.6.0", default-features = false } +# 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 } rand = { workspace = true, optional = true } @@ -60,12 +64,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 +98,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 ffe1477d514..5620d7b8f44 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..2c2caa712c7 --- /dev/null +++ b/stacks-common/src/util/secp256k1.rs @@ -0,0 +1,790 @@ +// 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::fmt; +use std::hash::{Hash, Hasher}; + +use k256::ecdsa::signature::hazmat::{PrehashSigner, 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 serde::de::{Deserialize, Error as de_Error}; +use serde::Serialize; + +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)] +pub enum Secp256k1Error { + InvalidKey, + InvalidSignature, + InvalidMessage, + InvalidRecoveryId, + SigningFailed, + RecoveryFailed, +} + +impl fmt::Display for Secp256k1Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Secp256k1Error::InvalidKey => write!(f, "Invalid key"), + Secp256k1Error::InvalidSignature => write!(f, "Invalid signature"), + Secp256k1Error::InvalidMessage => write!(f, "Invalid message"), + Secp256k1Error::InvalidRecoveryId => write!(f, "Invalid recovery ID"), + Secp256k1Error::SigningFailed => write!(f, "Signing failed"), + Secp256k1Error::RecoveryFailed => write!(f, "Recovery failed"), + } + } +} + +/// 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, 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: K256VerifyingKey, + 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: K256SigningKey, + compress_public: bool, +} + +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]; + if sig.len() < 65 { + buf[..sig.len()].copy_from_slice(sig); + } else { + buf.copy_from_slice(&sig[..65]); + } + 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]; + let recovery_id_byte = recid.to_byte(); + ret_bytes[0] = recovery_id_byte; + ret_bytes[1..=64].copy_from_slice(&bytes[..64]); + MessageSignature(ret_bytes) + } + + /// Converts to a secp256k1::ecdsa::RecoverableSignature. + pub fn to_secp256k1_recoverable(&self) -> Option { + let recovery_id = K256RecoveryId::from_byte(self.0[0])?; + let mut sig_bytes = [0u8; 64]; + sig_bytes[..64].copy_from_slice(&self.0[1..=64]); + let signature = K256Signature::from_slice(&sig_bytes).ok()?; + Some(RecoverableSignature { + signature, + recovery_id, + }) + } + + /// Converts from VRS to RSV. + pub fn to_rsv(&self) -> Vec { + [&self.0[1..], &self.0[0..1]].concat() + } +} + +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() + } +} + +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: K256Signature = self + .key + .sign_prehash(data_hash) + .map_err(|_| "Signing failed")?; + + // Try each recovery ID to find the correct one + for recovery_id in 0..4 { + if let Some(recovery_id) = K256RecoveryId::from_byte(recovery_id) { + if let Ok(recovered_key) = + K256VerifyingKey::recover_from_prehash(data_hash, &signature, recovery_id) + { + if recovered_key == *self.key.verifying_key() { + let recoverable_sig = RecoverableSignature { + signature, + recovery_id, + }; + return Ok(MessageSignature::from_secp256k1_recoverable( + &recoverable_sig, + )); + } + } + } + } + + Err("Failed to determine recovery ID") + } +} + +fn secp256k1_pubkey_serialize( + pubk: &K256VerifyingKey, + s: S, +) -> Result { + let public_key = K256PublicKey::from(pubk); + let encoded_point = public_key.to_encoded_point(true); // always serialize as compressed + let key_hex = to_hex(encoded_point.as_bytes()); + 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)?; + + let encoded_point = EncodedPoint::from_bytes(&key_bytes).map_err(de_Error::custom)?; + let public_key = + Option::::from(K256PublicKey::from_encoded_point(&encoded_point)) + .ok_or_else(|| de_Error::custom("Invalid public key"))?; + Ok(K256VerifyingKey::from(public_key)) +} + +fn secp256k1_privkey_serialize( + privk: &K256SigningKey, + s: S, +) -> Result { + let key_hex = to_hex(privk.to_bytes().as_slice()); + 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)?; + + if key_bytes.len() != 32 { + return Err(de_Error::custom("Private key must be 32 bytes")); + } + + let mut key_array = [0u8; 32]; + key_array.copy_from_slice(&key_bytes); + + let secret_key = + K256SecretKey::from_bytes(&GenericArray::from(key_array)).map_err(de_Error::custom)?; + + Ok(K256SigningKey::from(secret_key)) +} + +/// 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 super::*; + + #[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] + 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..64d56a41c3d --- /dev/null +++ b/stacks-common/src/util/secp256r1.rs @@ -0,0 +1,584 @@ +// 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 std::fmt; + +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 serde::de::{Deserialize, Error as de_Error}; +use serde::Serialize; + +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)] +pub enum Secp256r1Error { + InvalidKey, + InvalidSignature, + InvalidMessage, + InvalidRecoveryId, + SigningFailed, + RecoveryFailed, +} + +impl fmt::Display for Secp256r1Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Secp256r1Error::InvalidKey => write!(f, "Invalid key"), + Secp256r1Error::InvalidSignature => write!(f, "Invalid signature"), + Secp256r1Error::InvalidMessage => write!(f, "Invalid message"), + Secp256r1Error::InvalidRecoveryId => write!(f, "Invalid recovery ID"), + Secp256r1Error::SigningFailed => write!(f, "Signing failed"), + Secp256r1Error::RecoveryFailed => write!(f, "Recovery failed"), + } + } +} + +/// A Secp256r1 public key +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct Secp256r1PublicKey { + #[serde( + serialize_with = "secp256r1_pubkey_serialize", + deserialize_with = "secp256r1_pubkey_deserialize" + )] + key: P256VerifyingKey, + compressed: bool, +} + +/// A Secp256r1 private key +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct Secp256r1PrivateKey { + #[serde( + serialize_with = "secp256r1_privkey_serialize", + deserialize_with = "secp256r1_privkey_deserialize" + )] + key: P256SigningKey, + compress_public: bool, +} + +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) + } + + /// Converts to DER format + pub fn to_der(&self) -> Vec { + if let Ok(sig) = self.to_p256_signature() { + sig.to_der().as_bytes().to_vec() + } else { + vec![] + } + } +} + +impl Secp256r1PublicKey { + /// Generates a new random public key (for testing purposes only). + #[cfg(any(test, feature = "testing"))] + pub fn new() -> 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. + pub fn verify_digest( + &self, + msg_hash: &[u8], + sig: &MessageSignature, + ) -> Result { + if msg_hash.len() != 32 { + return Err("Invalid message: must be a 32-byte hash"); + } + + let p256_sig = sig + .to_p256_signature() + .map_err(|_| "Invalid signature: failed to decode signature")?; + + // Verify the signature + match self.key.verify(msg_hash, &p256_sig) { + Ok(()) => Ok(true), + Err(_) => Ok(false), + } + } +} + +#[cfg(any(test, feature = "testing"))] +impl Default for Secp256r1PublicKey { + fn default() -> Self { + Self::new() + } +} + +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[..]) { + // 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 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)) + } +} + +fn secp256r1_pubkey_serialize( + pubk: &P256VerifyingKey, + s: S, +) -> Result { + let public_key = P256PublicKey::from(pubk); + let encoded_point = public_key.to_encoded_point(true); // always serialize as compressed + let key_hex = to_hex(encoded_point.as_bytes()); + s.serialize_str(key_hex.as_str()) +} + +fn secp256r1_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)?; + + let encoded_point = EncodedPoint::from_bytes(&key_bytes).map_err(de_Error::custom)?; + let public_key = + Option::::from(P256PublicKey::from_encoded_point(&encoded_point)) + .ok_or_else(|| de_Error::custom("Invalid public key"))?; + Ok(P256VerifyingKey::from(public_key)) +} + +fn secp256r1_privkey_serialize( + privk: &P256SigningKey, + s: S, +) -> Result { + let key_hex = to_hex(privk.to_bytes().as_slice()); + s.serialize_str(key_hex.as_str()) +} + +fn secp256r1_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)?; + + if key_bytes.len() != 32 { + return Err(de_Error::custom("Private key must be 32 bytes")); + } + + let mut key_array = [0u8; 32]; + key_array.copy_from_slice(&key_bytes); + + let secret_key = + P256SecretKey::from_bytes(&GenericArray::from(key_array)).map_err(de_Error::custom)?; + + Ok(P256SigningKey::from(secret_key)) +} + +/// 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> { + if message_arr.len() != 32 { + return Err(Secp256r1Error::InvalidMessage); + } + + if signature_arr.len() != 64 { + return Err(Secp256r1Error::InvalidSignature); + } + + let encoded_point = + EncodedPoint::from_bytes(pubkey_arr).map_err(|_| Secp256r1Error::InvalidKey)?; + + let public_key = + Option::::from(P256PublicKey::from_encoded_point(&encoded_point)) + .ok_or(Secp256r1Error::InvalidKey)?; + let verifying_key = P256VerifyingKey::from(public_key); + + let signature = + P256Signature::from_slice(signature_arr).map_err(|_| Secp256r1Error::InvalidSignature)?; + + verifying_key + .verify(message_arr, &signature) + .map_err(|_| Secp256r1Error::InvalidSignature) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_serialize_compressed() { + let mut t1 = Secp256r1PrivateKey::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!(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_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(); + let valid = pubk.verify_digest(&msg_hash, &sig).unwrap(); + + assert!(valid); + } + + #[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 valid = pubk2.verify_digest(&msg_hash, &sig).unwrap(); + + assert!(!valid); + } + + #[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/src/chainstate/stacks/boot/costs-4.clar b/stackslib/src/chainstate/stacks/boot/costs-4.clar index 715bee49668..acc6b33bf2f 100644 --- a/stackslib/src/chainstate/stacks/boot/costs-4.clar +++ b/stackslib/src/chainstate/stacks/boot/costs-4.clar @@ -663,3 +663,6 @@ (define-read-only (cost_to_ascii (n uint)) (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark + +(define-read-only (cost_secp256r1verify (n uint)) + (runtime u1)) ;; TODO: needs criterion benchmark