From 3bc11669686fdf39cf506490f8072573c3b14a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Tue, 4 Jun 2024 16:59:23 +0200 Subject: [PATCH] Add k1 test functions (#17994) --- .../sources/crypto/ecdsa_k1.move | 49 ++++++++ .../tests/crypto/ecdsa_k1_tests.move | 83 ++++++++++++++ .../sui-move-natives/src/crypto/ecdsa_k1.rs | 107 +++++++++++++++++- .../latest/sui-move-natives/src/lib.rs | 10 ++ 4 files changed, 248 insertions(+), 1 deletion(-) diff --git a/crates/sui-framework/packages/sui-framework/sources/crypto/ecdsa_k1.move b/crates/sui-framework/packages/sui-framework/sources/crypto/ecdsa_k1.move index 6561756048785..39b6a1489ce3f 100644 --- a/crates/sui-framework/packages/sui-framework/sources/crypto/ecdsa_k1.move +++ b/crates/sui-framework/packages/sui-framework/sources/crypto/ecdsa_k1.move @@ -15,6 +15,21 @@ module sui::ecdsa_k1 { /// Error if the public key is invalid. const EInvalidPubKey: u64 = 2; + #[allow(unused_const)] + #[test_only] + /// Error if the private key is invalid. + const EInvalidPrivKey: u64 = 3; + + #[allow(unused_const)] + #[test_only] + /// Error if the given hash function does not exist. + const EInvalidHashFunction: u64 = 4; + + #[allow(unused_const)] + #[test_only] + /// Error if the seed is invalid. + const EInvalidSeed: u64 = 5; + #[allow(unused_const)] /// Hash function name that are valid for ecrecover and secp256k1_verify. const KECCAK256: u8 = 0; @@ -49,4 +64,38 @@ module sui::ecdsa_k1 { /// /// If the signature is valid to the pubkey and hashed message, return true. Else false. public native fun secp256k1_verify(signature: &vector, public_key: &vector, msg: &vector, hash: u8): bool; + + #[test_only] + /// @param private_key: A 32-bytes private key that is used to sign the message. + /// @param msg: The message to sign, this is raw message without hashing. + /// @param hash: The hash function used to hash the message when signing. + /// @param recoverable: A boolean flag to indicate if the produced signature should be recoverable. + /// + /// Return the signature in form (r, s) that is signed using Secp256k1. + /// If `recoverable` is true, the signature will be in form (r, s, v) where v is the recovery id. + /// + /// This should ONLY be used in tests, because it will reveal the private key onchain. + public native fun secp256k1_sign(private_key: &vector, msg: &vector, hash: u8, recoverable: bool): vector; + + #[test_only] + public struct KeyPair has drop { + private_key: vector, + public_key: vector, + } + + #[test_only] + public fun private_key(self: &KeyPair): &vector { + &self.private_key + } + + #[test_only] + public fun public_key(self: &KeyPair): &vector { + &self.public_key + } + + #[test_only] + /// @param seed: A 32-bytes seed that is used to generate the keypair. + /// + /// Returns a Secp256k1 keypair deterministically generated from the seed. + public native fun secp256k1_keypair_from_seed(seed: &vector): KeyPair; } diff --git a/crates/sui-framework/packages/sui-framework/tests/crypto/ecdsa_k1_tests.move b/crates/sui-framework/packages/sui-framework/tests/crypto/ecdsa_k1_tests.move index 4bb21a9f79d04..e8829935ee7d2 100644 --- a/crates/sui-framework/packages/sui-framework/tests/crypto/ecdsa_k1_tests.move +++ b/crates/sui-framework/packages/sui-framework/tests/crypto/ecdsa_k1_tests.move @@ -131,4 +131,87 @@ module sui::ecdsa_k1_tests { addr } + + #[test] + fun test_sign() { + let msg = b"Hello, world!"; + let pk = x"02337cca2171fdbfcfd657fa59881f46269f1e590b5ffab6023686c7ad2ecc2c1c"; + let sk = x"42258dcda14cf111c602b8971b8cc843e91e46ca905151c02744a6b017e69316"; + + // Test with Keccak256 hash + let sig = ecdsa_k1::secp256k1_sign(&sk, &msg, 0, false); + assert!(ecdsa_k1::secp256k1_verify(&sig, &pk, &msg, 0)); + assert!(sig == x"7e4237ebfbc36613e166bfc5f6229360a9c1949242da97ca04867e4de57b2df30c8340bcb320328cf46d71bda51fcb519e3ce53b348eec62de852e350edbd886"); + + // Test with SHA256 hash + let sig = ecdsa_k1::secp256k1_sign(&sk, &msg, 1, false); + assert!(ecdsa_k1::secp256k1_verify(&sig, &pk, &msg, 1)); + assert!(sig == x"e5847245b38548547f613aaea3421ad47f5b95a222366fb9f9b8c57568feb19c7077fc31e7d83e00acc1347d08c3e1ad50a4eeb6ab044f25c861ddc7be5b8f9f"); + + // Verification should fail with another message + let other_msg = b"Farewell, world!"; + assert!(!ecdsa_k1::secp256k1_verify(&sig, &pk, &other_msg, 0)); + } + + #[test] + fun test_sign_recoverable() { + let msg = b"Hello, world!"; + let pk = x"02337cca2171fdbfcfd657fa59881f46269f1e590b5ffab6023686c7ad2ecc2c1c"; + let sk = x"42258dcda14cf111c602b8971b8cc843e91e46ca905151c02744a6b017e69316"; + + // Test with Keccak256 hash + let sig = ecdsa_k1::secp256k1_sign(&sk, &msg, 0, true); + assert!(pk == ecdsa_k1::secp256k1_ecrecover(&sig, &msg, 0)); + + // Test with SHA256 hash + let sig = ecdsa_k1::secp256k1_sign(&sk, &msg, 1, true); + assert!(pk == ecdsa_k1::secp256k1_ecrecover(&sig, &msg, 1)); + + // Recoveres pk should not be the same with another message + let other_msg = b"Farewell, world!"; + assert!(pk != ecdsa_k1::secp256k1_ecrecover(&sig, &other_msg, 0)); + } + + #[test] + #[expected_failure(abort_code = ecdsa_k1::EInvalidHashFunction)] + fun test_sign_invalid_hash() { + let msg = b"Hello, world!"; + let sk = x"42258dcda14cf111c602b8971b8cc843e91e46ca905151c02744a6b017e69316"; + + // Invalid hash function + ecdsa_k1::secp256k1_sign(&sk, &msg, 2, false); + } + + #[test] + #[expected_failure(abort_code = ecdsa_k1::EInvalidPrivKey)] + fun test_sign_invalid_private_key() { + let msg = b"Hello, world!"; + + // Invalid (too short) private key + let sk = x"42258dcda14cf111c602b8971b8cc843e91e46ca905151c02744a6b017e693"; + + ecdsa_k1::secp256k1_sign(&sk, &msg, 0, false); + } + + #[test] + fun test_generate_keypair() { + let seed = b"Some random seed, 32 bytes long."; + let kp = ecdsa_k1::secp256k1_keypair_from_seed(&seed); + + let msg = b"Hello, world!"; + + let sig = ecdsa_k1::secp256k1_sign(kp.private_key(), &msg, 0, false); + assert!(ecdsa_k1::secp256k1_verify(&sig, kp.public_key(), &msg, 0)); + + let sig = ecdsa_k1::secp256k1_sign(kp.private_key(), &msg, 1, false); + assert!(ecdsa_k1::secp256k1_verify(&sig, kp.public_key(), &msg, 1)); + } + + #[test] + #[expected_failure(abort_code = ecdsa_k1::EInvalidSeed)] + fun test_generate_keypair_invalid_seed() { + let seed = b"Seed is not 32 bytes long"; + ecdsa_k1::secp256k1_keypair_from_seed(&seed); + } + } diff --git a/sui-execution/latest/sui-move-natives/src/crypto/ecdsa_k1.rs b/sui-execution/latest/sui-move-natives/src/crypto/ecdsa_k1.rs index 313df0a08cfca..9d728b10b5c02 100644 --- a/sui-execution/latest/sui-move-natives/src/crypto/ecdsa_k1.rs +++ b/sui-execution/latest/sui-move-natives/src/crypto/ecdsa_k1.rs @@ -1,6 +1,9 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 use crate::NativesCostTable; +use fastcrypto::secp256k1::Secp256k1KeyPair; +use fastcrypto::secp256k1::Secp256k1PrivateKey; +use fastcrypto::traits::RecoverableSigner; use fastcrypto::{ error::FastCryptoError, hash::{Keccak256, Sha256}, @@ -16,20 +19,27 @@ use move_vm_types::{ loaded_data::runtime_types::Type, natives::function::NativeResult, pop_arg, - values::{Value, VectorRef}, + values::{self, Value, VectorRef}, }; +use rand::rngs::StdRng; +use rand::SeedableRng; use smallvec::smallvec; use std::collections::VecDeque; +use sui_types::crypto::KeypairTraits; pub const FAIL_TO_RECOVER_PUBKEY: u64 = 0; pub const INVALID_SIGNATURE: u64 = 1; pub const INVALID_PUBKEY: u64 = 2; +pub const INVALID_PRIVKEY: u64 = 3; +pub const INVALID_HASH_FUNCTION: u64 = 4; +pub const INVALID_SEED: u64 = 5; pub const KECCAK256: u8 = 0; pub const SHA256: u8 = 1; const KECCAK256_BLOCK_SIZE: usize = 136; const SHA256_BLOCK_SIZE: usize = 64; +const SEED_LENGTH: usize = 32; #[derive(Clone)] pub struct EcdsaK1EcrecoverCostParams { @@ -286,3 +296,98 @@ pub fn secp256k1_verify( Ok(NativeResult::ok(cost, smallvec![Value::bool(result)])) } + +/*************************************************************************************************** + * native fun secp256k1_sign (TEST ONLY) + * Implementation of the Move native function `secp256k1_sign(private_key: &vector, msg: &vector, hash: u8): vector` + * This function has two cost modes depending on the hash being set to`KECCAK256` or `SHA256`. The core formula is same but constants differ. + * If hash = 0, we use the `keccak256` cost constants, otherwise we use the `sha256` cost constants. + * gas cost: 0 (because it is only for test purposes) + **************************************************************************************************/ +pub fn secp256k1_sign( + _context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 4); + + // The corresponding Move function, sui::ecdsa_k1::secp256k1_sign, is only used for testing, so + // we don't need to charge any gas. + let cost = 0.into(); + + let recoverable = pop_arg!(args, bool); + let hash = pop_arg!(args, u8); + let msg = pop_arg!(args, VectorRef); + let private_key_bytes = pop_arg!(args, VectorRef); + + let msg_ref = msg.as_bytes_ref(); + let private_key_bytes_ref = private_key_bytes.as_bytes_ref(); + + let sk = match ::from_bytes(&private_key_bytes_ref) { + Ok(sk) => sk, + Err(_) => return Ok(NativeResult::err(cost, INVALID_PRIVKEY)), + }; + + let kp = Secp256k1KeyPair::from(sk); + + let signature = match (hash, recoverable) { + (KECCAK256, true) => kp + .sign_recoverable_with_hash::(&msg_ref) + .as_bytes() + .to_vec(), + (KECCAK256, false) => kp.sign_with_hash::(&msg_ref).as_bytes().to_vec(), + (SHA256, true) => kp + .sign_recoverable_with_hash::(&msg_ref) + .as_bytes() + .to_vec(), + (SHA256, false) => kp.sign_with_hash::(&msg_ref).as_bytes().to_vec(), + _ => return Ok(NativeResult::err(cost, INVALID_HASH_FUNCTION)), + }; + + Ok(NativeResult::ok( + cost, + smallvec![Value::vector_u8(signature)], + )) +} + +/*************************************************************************************************** + * native fun secp256k1_keypair_from_seed (TEST ONLY) + * Implementation of the Move native function `secp256k1_sign(seed: &vector): KeyPair` + * Seed must be exactly 32 bytes long. + * gas cost: 0 (because it is only for test purposes) + **************************************************************************************************/ +pub fn secp256k1_keypair_from_seed( + _context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert!(ty_args.is_empty()); + debug_assert!(args.len() == 1); + + // The corresponding Move function, sui::ecdsa_k1::secp256k1_keypair_from_seed, is only used for + // testing, so we don't need to charge any gas. + let cost = 0.into(); + + let seed = pop_arg!(args, VectorRef); + let seed_ref = seed.as_bytes_ref(); + + if seed_ref.len() != SEED_LENGTH { + return Ok(NativeResult::err(cost, INVALID_SEED)); + } + let mut seed_array = [0u8; SEED_LENGTH]; + seed_array.clone_from_slice(&seed_ref); + + let kp = Secp256k1KeyPair::generate(&mut StdRng::from_seed(seed_array)); + + let pk_bytes = kp.public().as_bytes().to_vec(); + let sk_bytes = kp.private().as_bytes().to_vec(); + + Ok(NativeResult::ok( + cost, + smallvec![Value::struct_(values::Struct::pack(vec![ + Value::vector_u8(sk_bytes), + Value::vector_u8(pk_bytes), + ]))], + )) +} diff --git a/sui-execution/latest/sui-move-natives/src/lib.rs b/sui-execution/latest/sui-move-natives/src/lib.rs index 20c7ecb7f61f4..76267d1d603bf 100644 --- a/sui-execution/latest/sui-move-natives/src/lib.rs +++ b/sui-execution/latest/sui-move-natives/src/lib.rs @@ -905,6 +905,16 @@ pub fn all_natives(silent: bool) -> NativeFunctionTable { "hash_to_input_internal", make_native!(vdf::hash_to_input_internal), ), + ( + "ecdsa_k1", + "secp256k1_sign", + make_native!(ecdsa_k1::secp256k1_sign), + ), + ( + "ecdsa_k1", + "secp256k1_keypair_from_seed", + make_native!(ecdsa_k1::secp256k1_keypair_from_seed), + ), ]; let sui_framework_natives_iter = sui_framework_natives