Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add Move contract and tests for ecrecover to address #4543

Merged
merged 1 commit into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ expression: common_costs
"storage_rebate": 15
},
"SplitCoin": {
"computation_cost": 575,
"computation_cost": 576,
"storage_cost": 80,
"storage_rebate": 0
},
Expand Down
6 changes: 6 additions & 0 deletions crates/sui-framework/sources/crypto.move
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ module sui::crypto {
/// applied to Secp256k1 signatures.
public native fun ecrecover(signature: vector<u8>, hashed_msg: vector<u8>): vector<u8>;

/// @param pubkey: A 33-bytes compressed public key, a prefix either 0x02 or 0x03 and a 256-bit integer.
///
/// If the compressed public key is valid, return the 65-bytes uncompressed public key,
/// otherwise throw error.
public native fun decompress_pubkey(pubkey: vector<u8>): vector<u8>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not to be addressed in this PR, but some ideas for improving this library)

  • We could use type wrappers to represent compressed and decompressed public keys, signatures, etc. instead of always using vector<u8>. This should make it easier to avoid common mistakes/misuse
  • Instead of having one big crypto library, we could split it into a few smaller ones partitioned by (e.g.) signature scheme. No strong priors on how this should be done, but I think it will be easier to add new schemes in the future if we can do so by adding a fresh module instead of upgrading an existing on-chain module

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, will follow up a PR: #4567


/// @param data: arbitrary bytes data to hash
/// Hash the input bytes using keccak256 and returns 32 bytes.
public native fun keccak256(data: vector<u8>): vector<u8>;
Expand Down
57 changes: 48 additions & 9 deletions crates/sui-framework/src/natives/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use crate::{legacy_emit_cost, legacy_empty_cost};
use curve25519_dalek_ng::scalar::Scalar;
use fastcrypto::{
bls12381::{BLS12381PublicKey, BLS12381Signature},
Expand All @@ -16,15 +17,15 @@ use move_vm_types::{
};
use smallvec::smallvec;
use std::collections::VecDeque;

use crate::{legacy_emit_cost, legacy_empty_cost};
use sui_types::error::SuiError;

pub const FAIL_TO_RECOVER_PUBKEY: u64 = 0;
pub const INVALID_SIGNATURE: u64 = 1;
pub const INVALID_BULLETPROOF: u64 = 2;
pub const INVALID_RISTRETTO_GROUP_ELEMENT: u64 = 3;
pub const INVALID_RISTRETTO_SCALAR: u64 = 4;
pub const BULLETPROOFS_VERIFICATION_FAILED: u64 = 5;
pub const INVALID_PUBKEY: u64 = 6;

pub const BP_DOMAIN: &[u8] = b"mizu";

Expand All @@ -41,15 +42,53 @@ pub fn ecrecover(
let signature = pop_arg!(args, Vec<u8>);
// TODO: implement native gas cost estimation https://github.com/MystenLabs/sui/issues/3593
let cost = legacy_empty_cost();
match recover_pubkey(signature, hashed_msg) {
Ok(pubkey) => Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(pubkey.as_bytes().to_vec())],
)),
Err(SuiError::InvalidSignature { error: _ }) => {
Ok(NativeResult::err(cost, INVALID_SIGNATURE))
}
Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)),
}
}

fn recover_pubkey(signature: Vec<u8>, hashed_msg: Vec<u8>) -> Result<Secp256k1PublicKey, SuiError> {
match <Secp256k1Signature as ToFromBytes>::from_bytes(&signature) {
Ok(signature) => match signature.recover(&hashed_msg) {
Ok(pubkey) => Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(pubkey.as_bytes().to_vec())],
)),
Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)),
Ok(pubkey) => Ok(pubkey),
Err(e) => Err(SuiError::KeyConversionError(e.to_string())),
},
Err(_) => Ok(NativeResult::err(cost, INVALID_SIGNATURE)),
Err(e) => Err(SuiError::InvalidSignature {
error: e.to_string(),
}),
}
}

/// Convert a compressed 33-bytes Secp256k1 pubkey to an 65-bytes uncompressed one.
pub fn decompress_pubkey(
_context: &mut NativeContext,
ty_args: Vec<Type>,
mut args: VecDeque<Value>,
) -> PartialVMResult<NativeResult> {
debug_assert!(ty_args.is_empty());
debug_assert!(args.len() == 1);

let pubkey = pop_arg!(args, Vec<u8>);

// TODO: implement native gas cost estimation https://github.com/MystenLabs/sui/issues/3593
let cost = legacy_empty_cost();

match Secp256k1PublicKey::from_bytes(&pubkey) {
Ok(pubkey) => {
let uncompressed = &pubkey.pubkey.serialize_uncompressed();
Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(uncompressed.to_vec())],
))
}
Err(_) => Ok(NativeResult::err(cost, INVALID_PUBKEY)),
}
}

Expand All @@ -68,7 +107,7 @@ pub fn keccak256(
Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(
<sha3::Keccak256 as sha3::digest::Digest>::digest(msg)
<sha3::Keccak256 as sha3::digest::Digest>::digest(&msg)
.as_slice()
.to_vec()
)],
Expand Down
5 changes: 5 additions & 0 deletions crates/sui-framework/src/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pub fn all_natives(
) -> NativeFunctionTable {
let sui_natives: &[(&str, &str, NativeFunction)] = &[
("crypto", "ecrecover", make_native!(crypto::ecrecover)),
(
"crypto",
"decompress_pubkey",
make_native!(crypto::decompress_pubkey),
),
("crypto", "keccak256", make_native!(crypto::keccak256)),
(
"crypto",
Expand Down
88 changes: 87 additions & 1 deletion crates/sui-framework/tests/crypto_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
module sui::crypto_tests {
use sui::crypto;
use sui::elliptic_curve as ec;
use std::vector;

#[test]
fun test_ecrecover_pubkey() {
// test case generated against https://docs.rs/secp256k1/latest/secp256k1/
Expand All @@ -16,6 +18,20 @@ module sui::crypto_tests {
assert!(pubkey == pubkey_bytes, 0);
}

#[test]
fun test_ecrecover_pubkey_2() {
// Test case from go-ethereum: https://github.com/ethereum/go-ethereum/blob/master/crypto/signature_test.go#L37
// hashed_msg: 0xce0677bb30baa8cf067c88db9811f4333d131bf8bcf12fe7065d211dce971008
// sig: 0x90f27b8b488db00b00606796d2987f6a5f59ae62ea05effe84fef5b8b0e549984a691139ad57a3f0b906637673aa2f63d1f55cb1a69199d4009eea23ceaddc9301
// pubkey: 0x02e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a
let hashed_msg = vector[206, 6, 119, 187, 48, 186, 168, 207, 6, 124, 136, 219, 152, 17, 244, 51, 61, 19, 27, 248, 188, 241, 47, 231, 6, 93, 33, 29, 206, 151, 16, 8];
let sig = vector[144, 242, 123, 139, 72, 141, 176, 11, 0, 96, 103, 150, 210, 152, 127, 106, 95, 89, 174, 98, 234, 5, 239, 254, 132, 254, 245, 184, 176, 229, 73, 152, 74, 105, 17, 57, 173, 87, 163, 240, 185, 6, 99, 118, 115, 170, 47, 99, 209, 245, 92, 177, 166, 145, 153, 212, 0, 158, 234, 35, 206, 173, 220, 147, 1];
let pubkey_bytes = vector[2, 227, 45, 244, 40, 101, 233, 113, 53, 172, 251, 101, 243, 186, 231, 27, 220, 134, 244, 212, 145, 80, 173, 106, 68, 11, 111, 21, 135, 129, 9, 136, 10];

let pubkey = crypto::ecrecover(sig, hashed_msg);
assert!(pubkey == pubkey_bytes, 0);
}

#[test]
#[expected_failure(abort_code = 0)]
fun test_ecrecover_pubkey_fail_to_recover() {
Expand Down Expand Up @@ -240,4 +256,74 @@ module sui::crypto_tests {
let verify = crypto::secp256k1_verify(sig, pk, msg);
assert!(verify == false, 0)
}
}

#[test]
fun test_ecrecover_eth_address() {
// Due to the lack of conversion tool in Move, here we convert hex to vector in python3: [x for x in bytearray.fromhex(hex_string[2:])]
// Test case from https://web3js.readthedocs.io/en/v1.7.5/web3-eth-accounts.html#recover
// signature: 0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c
// hashed_msg: 0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655
// address: 0x2c7536e3605d9c16a7a3d7b1898e529396a65c23
let sig = vector[185, 20, 103, 229, 112, 166, 70, 106, 169, 233, 135, 108, 188, 208, 19, 186, 186, 2, 144, 11, 137, 121, 212, 63, 226, 8, 164, 164, 243, 57, 245, 253, 96, 7, 231, 76, 216, 46, 3, 123, 128, 1, 134, 66, 47, 194, 218, 22, 124, 116, 126, 240, 69, 229, 209, 138, 95, 93, 67, 0, 248, 225, 160, 41, 28];
let hashed_msg = vector[29, 164, 75, 88, 110, 176, 114, 159, 247, 10, 115, 195, 38, 146, 111, 110, 213, 162, 95, 91, 5, 110, 127, 71, 251, 198, 229, 141, 134, 135, 22, 85];
let addr1 = vector[44, 117, 54, 227, 96, 93, 156, 22, 167, 163, 215, 177, 137, 142, 82, 147, 150, 166, 92, 35];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);

// Test case from https://etherscan.io/verifySig/9754
// sig: 0xcb614cba67d6a37b9cb90d21635d81ed035b8ccb99f0befe05495b819111119b17ecf0c0cb4bcc781de387206f6dfcd9f1b99e1b54b44c376412d8f5c919b1981b
// hashed_msg: 0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655
// addr: 0x4cbf668fca6f10d01f161122534044436b80702e
let sig = vector[203, 97, 76, 186, 103, 214, 163, 123, 156, 185, 13, 33, 99, 93, 129, 237, 3, 91, 140, 203, 153, 240, 190, 254, 5, 73, 91, 129, 145, 17, 17, 155, 23, 236, 240, 192, 203, 75, 204, 120, 29, 227, 135, 32, 111, 109, 252, 217, 241, 185, 158, 27, 84, 180, 76, 55, 100, 18, 216, 245, 201, 25, 177, 152, 27];
let hashed_msg = vector[29, 164, 75, 88, 110, 176, 114, 159, 247, 10, 115, 195, 38, 146, 111, 110, 213, 162, 95, 91, 5, 110, 127, 71, 251, 198, 229, 141, 134, 135, 22, 85];
let addr1 = vector[76, 191, 102, 143, 202, 111, 16, 208, 31, 22, 17, 34, 83, 64, 68, 67, 107, 128, 112, 46];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);

// Test case from https://goerli.etherscan.io/tx/0x18f72457b356f367db214de9dda07f5d253ebfeb5c426b0d9d5b346b4ba8d021
// sig: 0x8e809da5ca76e6371ba8dcaa748fc2973f0d9862f76ed08f55b869f5e73591dd24a7367f1ee9e6e3723d13bb0a7092fafb8851f7eecd4a8d34c977013e1551482e
// hashed_msg: 0x529283629f75203330f0acf68bdbc4e879047fe75da8071c079c495bbb9fb78a
// addr: 0x4cbf668fca6f10d01f161122534044436b80702e
let sig = vector[142, 128, 157, 165, 202, 118, 230, 55, 27, 168, 220, 170, 116, 143, 194, 151, 63, 13, 152, 98, 247, 110, 208, 143, 85, 184, 105, 245, 231, 53, 145, 221, 36, 167, 54, 127, 30, 233, 230, 227, 114, 61, 19, 187, 10, 112, 146, 250, 251, 136, 81, 247, 238, 205, 74, 141, 52, 201, 119, 1, 62, 21, 81, 72, 46];
let hashed_msg = vector[82, 146, 131, 98, 159, 117, 32, 51, 48, 240, 172, 246, 139, 219, 196, 232, 121, 4, 127, 231, 93, 168, 7, 28, 7, 156, 73, 91, 187, 159, 183, 138];
let addr1 = vector[76, 191, 102, 143, 202, 111, 16, 208, 31, 22, 17, 34, 83, 64, 68, 67, 107, 128, 112, 46];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);
}

// Helper Move function to recover signature directly to an ETH address.
fun ecrecover_eth_address(sig: vector<u8>, hashed_msg: vector<u8>): vector<u8> {
// Normalize the last byte of the signature to be 0 or 1.
let v = vector::borrow_mut(&mut sig, 64);
if (*v == 27) {
*v = 0;
} else if (*v == 28) {
*v = 1;
} else if (*v > 35) {
*v = (*v - 1) % 2;
};

let pubkey = crypto::ecrecover(sig, hashed_msg);
let uncompressed = crypto::decompress_pubkey(pubkey);

// Take the last 64 bytes of the uncompressed pubkey.
let uncompressed_64 = vector::empty<u8>();
let i = 1;
while (i < 65) {
let value = vector::borrow(&uncompressed, i);
vector::push_back(&mut uncompressed_64, *value);
i = i + 1;
};

// Take the last 20 bytes of the hash of the 64-bytes uncompressed pubkey.
let hashed = crypto::keccak256(uncompressed_64);
let addr = vector::empty<u8>();
let i = 12;
while (i < 32) {
let value = vector::borrow(&hashed, i);
vector::push_back(&mut addr, *value);
i = i + 1;
};
addr
}
}
44 changes: 43 additions & 1 deletion sui_programmability/examples/math/sources/ecdsa.move
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module math::ecdsa {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
use sui::transfer;

use std::vector;
/// Event on whether the signature is verified
struct VerifiedEvent has copy, drop {
is_verified: bool,
Expand Down Expand Up @@ -41,6 +41,48 @@ module math::ecdsa {
transfer::transfer(pubkey, recipient)
}

public entry fun ecrecover_to_eth_address(signature: vector<u8>, hashed_msg: vector<u8>, recipient: address, ctx: &mut TxContext) {
// Normalize the last byte of the signature to be 0 or 1.
let v = vector::borrow_mut(&mut signature, 64);
if (*v == 27) {
*v = 0;
} else if (*v == 28) {
*v = 1;
} else if (*v > 35) {
*v = (*v - 1) % 2;
};

let pubkey = crypto::ecrecover(signature, hashed_msg);
let uncompressed = crypto::decompress_pubkey(pubkey);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! and good observation that in Eth they hash the uncompressed pub key.


// Take the last 64 bytes of the uncompressed pubkey.
let uncompressed_64 = vector::empty<u8>();
let i = 1;
while (i < 65) {
let value = vector::borrow(&uncompressed, i);
vector::push_back(&mut uncompressed_64, *value);
i = i + 1;
};

// Take the last 20 bytes of the hash of the 64-bytes uncompressed pubkey.
let hashed = crypto::keccak256(uncompressed_64);
let addr = vector::empty<u8>();
let i = 12;
while (i < 32) {
let value = vector::borrow(&hashed, i);
vector::push_back(&mut addr, *value);
i = i + 1;
};

let addr_object = Output {
id: object::new(ctx),
value: addr,
};

// Transfer an output data object holding the address to the recipient.
transfer::transfer(addr_object, recipient)
}

public entry fun secp256k1_verify(signature: vector<u8>, public_key: vector<u8>, hashed_msg: vector<u8>) {
event::emit(VerifiedEvent {is_verified: crypto::secp256k1_verify(signature, public_key, hashed_msg)});
}
Expand Down