Skip to content

Commit

Permalink
Add k1 test functions (MystenLabs#17994)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-lj authored Jun 4, 2024
1 parent 2548353 commit 3bc1166
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u8>, public_key: &vector<u8>, msg: &vector<u8>, 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<u8>, msg: &vector<u8>, hash: u8, recoverable: bool): vector<u8>;

#[test_only]
public struct KeyPair has drop {
private_key: vector<u8>,
public_key: vector<u8>,
}

#[test_only]
public fun private_key(self: &KeyPair): &vector<u8> {
&self.private_key
}

#[test_only]
public fun public_key(self: &KeyPair): &vector<u8> {
&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<u8>): KeyPair;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
107 changes: 106 additions & 1 deletion sui-execution/latest/sui-move-natives/src/crypto/ecdsa_k1.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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 {
Expand Down Expand Up @@ -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<u8>, msg: &vector<u8>, hash: u8): vector<u8>`
* 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<Type>,
mut args: VecDeque<Value>,
) -> PartialVMResult<NativeResult> {
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 <Secp256k1PrivateKey as ToFromBytes>::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::<Keccak256>(&msg_ref)
.as_bytes()
.to_vec(),
(KECCAK256, false) => kp.sign_with_hash::<Keccak256>(&msg_ref).as_bytes().to_vec(),
(SHA256, true) => kp
.sign_recoverable_with_hash::<Sha256>(&msg_ref)
.as_bytes()
.to_vec(),
(SHA256, false) => kp.sign_with_hash::<Sha256>(&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<u8>): 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<Type>,
mut args: VecDeque<Value>,
) -> PartialVMResult<NativeResult> {
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),
]))],
))
}
10 changes: 10 additions & 0 deletions sui-execution/latest/sui-move-natives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3bc1166

Please sign in to comment.