From f58039b682187d7694a8f30fe647389a75354733 Mon Sep 17 00:00:00 2001 From: 10gic <2391796+10gic@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:47:15 +0800 Subject: [PATCH 1/3] Support Sui sign personal message --- include/TrustWalletCore/TWMessageSigner.h | 8 ++ rust/chains/tw_sui/Cargo.toml | 1 + rust/chains/tw_sui/src/entry.rs | 9 +- rust/chains/tw_sui/src/modules/intent.rs | 95 +++++++++++++++ .../tw_sui/src/modules/message_signer.rs | 115 ++++++++++++++++++ rust/chains/tw_sui/src/modules/mod.rs | 2 + rust/chains/tw_sui/src/modules/tx_signer.rs | 63 +--------- rust/chains/tw_sui/src/signature.rs | 34 +++++- rust/tw_tests/tests/chains/sui/mod.rs | 1 + .../tests/chains/sui/sui_message_sign.rs | 80 ++++++++++++ src/interface/TWMessageSigner.cpp | 13 ++ src/proto/Sui.proto | 37 ++++++ tests/chains/Sui/MessageSignerTests.cpp | 75 ++++++++++++ 13 files changed, 467 insertions(+), 66 deletions(-) create mode 100644 rust/chains/tw_sui/src/modules/intent.rs create mode 100644 rust/chains/tw_sui/src/modules/message_signer.rs create mode 100644 rust/tw_tests/tests/chains/sui/sui_message_sign.rs create mode 100644 tests/chains/Sui/MessageSignerTests.cpp diff --git a/include/TrustWalletCore/TWMessageSigner.h b/include/TrustWalletCore/TWMessageSigner.h index c7f3c189edf..c05cf41d556 100644 --- a/include/TrustWalletCore/TWMessageSigner.h +++ b/include/TrustWalletCore/TWMessageSigner.h @@ -14,6 +14,14 @@ TW_EXTERN_C_BEGIN TW_EXPORT_CLASS struct TWMessageSigner; +/// Computes preimage hashes of a message, needed for external signing. +/// +/// \param coin The given coin type to sign the message for. +/// \param input The serialized data of a `MessageSigningInput` proto object, (e.g. `TW.Solana.Proto.MessageSigningInput`). +/// \return The serialized data of a `PreSigningOutput` proto object, (e.g. `TxCompiler::Proto::PreSigningOutput`). +TW_EXPORT_STATIC_METHOD +TWData* _Nullable TWMessageSignerPreImageHashes(enum TWCoinType coin, TWData* _Nonnull input); + /// Signs an arbitrary message to prove ownership of an address for off-chain services. /// /// \param coin The given coin type to sign the message for. diff --git a/rust/chains/tw_sui/Cargo.toml b/rust/chains/tw_sui/Cargo.toml index 1aa3d7cabb3..717b4e236a0 100644 --- a/rust/chains/tw_sui/Cargo.toml +++ b/rust/chains/tw_sui/Cargo.toml @@ -13,4 +13,5 @@ tw_encoding = { path = "../../tw_encoding" } tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } tw_proto = { path = "../../tw_proto" } diff --git a/rust/chains/tw_sui/src/entry.rs b/rust/chains/tw_sui/src/entry.rs index 5c0539c9e29..24a3b9d6ff7 100644 --- a/rust/chains/tw_sui/src/entry.rs +++ b/rust/chains/tw_sui/src/entry.rs @@ -4,6 +4,7 @@ use crate::address::SuiAddress; use crate::compiler::SuiCompiler; +use crate::modules::message_signer::SuiMessageSigner; use crate::modules::transaction_util::SuiTransactionUtil; use crate::signer::SuiSigner; use std::str::FromStr; @@ -12,7 +13,6 @@ use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; use tw_coin_entry::derivation::Derivation; use tw_coin_entry::error::prelude::*; use tw_coin_entry::modules::json_signer::NoJsonSigner; -use tw_coin_entry::modules::message_signer::NoMessageSigner; use tw_coin_entry::modules::plan_builder::NoPlanBuilder; use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; use tw_coin_entry::modules::wallet_connector::NoWalletConnector; @@ -33,7 +33,7 @@ impl CoinEntry for SuiEntry { // Optional modules: type JsonSigner = NoJsonSigner; type PlanBuilder = NoPlanBuilder; - type MessageSigner = NoMessageSigner; + type MessageSigner = SuiMessageSigner; type WalletConnector = NoWalletConnector; type TransactionDecoder = NoTransactionDecoder; type TransactionUtil = SuiTransactionUtil; @@ -92,6 +92,11 @@ impl CoinEntry for SuiEntry { SuiCompiler::compile(coin, input, signatures, public_keys) } + #[inline] + fn message_signer(&self) -> Option { + Some(SuiMessageSigner) + } + #[inline] fn transaction_util(&self) -> Option { Some(SuiTransactionUtil) diff --git a/rust/chains/tw_sui/src/modules/intent.rs b/rust/chains/tw_sui/src/modules/intent.rs new file mode 100644 index 00000000000..4e62230a236 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/intent.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use serde::Serialize; +use serde_repr::Serialize_repr; + +/// Code snippets from: +/// https://github.com/MystenLabs/sui/blob/a16c942b72c13f42846b3c543b6622af85a5f634/crates/shared-crypto/src/intent.rs + +/// This enums specifies the intent scope. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentScope { + /// Used for a user signature on a transaction data. + TransactionData = 0, + /// Used for a user signature on a personal message. + PersonalMessage = 3, +} + +/// The version here is to distinguish between signing different versions of the struct +/// or enum. Serialized output between two different versions of the same struct/enum +/// might accidentally (or maliciously on purpose) match. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum IntentVersion { + V0 = 0, +} + +/// This enums specifies the application ID. Two intents in two different applications +/// (i.e., Narwhal, Sui, Ethereum etc) should never collide, so that even when a signing +/// key is reused, nobody can take a signature designated for app_1 and present it as a +/// valid signature for an (any) intent in app_2. +#[derive(Serialize_repr)] +#[repr(u8)] +pub enum AppId { + Sui = 0, +} + +/// An intent is a compact struct serves as the domain separator for a message that a signature commits to. +/// It consists of three parts: [enum IntentScope] (what the type of the message is), +/// [enum IntentVersion], [enum AppId] (what application that the signature refers to). +/// It is used to construct [struct IntentMessage] that what a signature commits to. +/// +/// The serialization of an Intent is a 3-byte array where each field is represented by a byte. +#[derive(Serialize)] +pub struct Intent { + pub scope: IntentScope, + pub version: IntentVersion, + pub app_id: AppId, +} + +impl Intent { + pub fn sui_transaction() -> Self { + Self { + scope: IntentScope::TransactionData, + version: IntentVersion::V0, + app_id: AppId::Sui, + } + } + + pub fn personal_message() -> Self { + Self { + scope: IntentScope::PersonalMessage, + version: IntentVersion::V0, + app_id: AppId::Sui, + } + } +} + +/// Intent Message is a wrapper around a message with its intent. The message can +/// be any type that implements [trait Serialize]. *ALL* signatures in Sui must commits +/// to the intent message, not the message itself. This guarantees any intent +/// message signed in the system cannot collide with another since they are domain +/// separated by intent. +/// +/// The serialization of an IntentMessage is compact: it only appends three bytes +/// to the message itself. +#[derive(Serialize)] +pub struct IntentMessage { + pub intent: Intent, + pub value: T, +} + +impl IntentMessage { + pub fn new(intent: Intent, value: T) -> Self { + Self { intent, value } + } +} + +/// A person message that wraps around a byte array. +#[derive(Serialize)] +pub struct PersonalMessage { + pub message: Vec, +} diff --git a/rust/chains/tw_sui/src/modules/message_signer.rs b/rust/chains/tw_sui/src/modules/message_signer.rs new file mode 100644 index 00000000000..13c29e61952 --- /dev/null +++ b/rust/chains/tw_sui/src/modules/message_signer.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::intent::{Intent, IntentMessage, PersonalMessage}; +use crate::signature::SuiSignatureInfo; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::message_signer::MessageSigner; +use tw_coin_entry::signing_output_error; +use tw_encoding::bcs; +use tw_hash::blake2::blake2_b; +use tw_hash::H256; +use tw_keypair::ed25519; +use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait, VerifyingKeyTrait}; +use tw_memory::Data; +use tw_misc::traits::ToBytesVec; +use tw_misc::try_or_false; +use tw_proto::Sui::Proto; +use tw_proto::TxCompiler::Proto as CompilerProto; + +pub struct SuiMessageSigner; + +/// Sui personal message signer. +/// Here is an example of how to sign a message: +/// https://github.com/MystenLabs/sui/blob/a16c942b72c13f42846b3c543b6622af85a5f634/crates/sui-types/src/unit_tests/utils.rs#L201 +impl SuiMessageSigner { + pub fn sign_message_impl( + _coin: &dyn CoinContext, + input: Proto::MessageSigningInput, + ) -> SigningResult> { + let key_pair = ed25519::sha512::KeyPair::try_from(input.private_key.as_ref())?; + + let hash = Self::message_preimage_hashes_impl(input.message.as_bytes().into())?; + + let signature = key_pair.sign(hash.to_vec())?; + let signature_info = SuiSignatureInfo::ed25519(&signature, key_pair.public()); + + Ok(Proto::MessageSigningOutput { + signature: signature_info.to_base64().into(), + ..Proto::MessageSigningOutput::default() + }) + } + + pub fn message_preimage_hashes_impl(message: Data) -> SigningResult { + let data = PersonalMessage { message }; + let intent_msg = IntentMessage::new(Intent::personal_message(), data); + + let data_to_sign = bcs::encode(&intent_msg).tw_err(|_| SigningErrorType::Error_internal)?; + + let data_to_sign = blake2_b(&data_to_sign, H256::LEN) + .and_then(|hash| H256::try_from(hash.as_slice())) + .tw_err(|_| SigningErrorType::Error_internal)?; + + Ok(data_to_sign) + } +} + +impl MessageSigner for SuiMessageSigner { + type MessageSigningInput<'a> = Proto::MessageSigningInput<'a>; + type MessagePreSigningOutput = CompilerProto::PreSigningOutput<'static>; + type MessageSigningOutput = Proto::MessageSigningOutput<'static>; + type MessageVerifyingInput<'a> = Proto::MessageVerifyingInput<'a>; + + fn message_preimage_hashes( + &self, + _coin: &dyn CoinContext, + input: Self::MessageSigningInput<'_>, + ) -> Self::MessagePreSigningOutput { + let hash = match Self::message_preimage_hashes_impl(input.message.as_bytes().into()) { + Ok(hash) => hash, + Err(e) => return signing_output_error!(CompilerProto::PreSigningOutput, e), + }; + + CompilerProto::PreSigningOutput { + data: hash.to_vec().into(), + data_hash: hash.to_vec().into(), + ..CompilerProto::PreSigningOutput::default() + } + } + + fn sign_message( + &self, + coin: &dyn CoinContext, + input: Self::MessageSigningInput<'_>, + ) -> Self::MessageSigningOutput { + Self::sign_message_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::MessageSigningOutput, e)) + } + + fn verify_message( + &self, + _coin: &dyn CoinContext, + input: Self::MessageVerifyingInput<'_>, + ) -> bool { + let signature_info = try_or_false!(SuiSignatureInfo::from_base64(input.signature.as_ref())); + let public_key = try_or_false!(ed25519::sha512::PublicKey::try_from( + input.public_key.as_ref() + )); + + // Check if the public key in the signature matches the public key in the input. + if signature_info.public_key.ne(&public_key.to_bytes()) { + return false; + } + let signature = try_or_false!(ed25519::Signature::try_from( + signature_info.signature.as_slice() + )); + let hash = try_or_false!(Self::message_preimage_hashes_impl( + input.message.as_bytes().to_vec() + )); + + // Verify the signature. + public_key.verify(signature, hash.to_vec()) + } +} diff --git a/rust/chains/tw_sui/src/modules/mod.rs b/rust/chains/tw_sui/src/modules/mod.rs index 32f928c8ec3..9574c876769 100644 --- a/rust/chains/tw_sui/src/modules/mod.rs +++ b/rust/chains/tw_sui/src/modules/mod.rs @@ -2,6 +2,8 @@ // // Copyright © 2017 Trust Wallet. +pub mod intent; +pub mod message_signer; pub mod transaction_util; pub mod tx_builder; pub mod tx_signer; diff --git a/rust/chains/tw_sui/src/modules/tx_signer.rs b/rust/chains/tw_sui/src/modules/tx_signer.rs index 34eb476f48b..31270b2277f 100644 --- a/rust/chains/tw_sui/src/modules/tx_signer.rs +++ b/rust/chains/tw_sui/src/modules/tx_signer.rs @@ -3,10 +3,9 @@ // Copyright © 2017 Trust Wallet. use crate::address::SuiAddress; +use crate::modules::intent::Intent; use crate::signature::SuiSignatureInfo; use crate::transaction::transaction_data::TransactionData; -use serde::Serialize; -use serde_repr::Serialize_repr; use tw_coin_entry::error::prelude::*; use tw_encoding::bcs; use tw_hash::blake2::blake2_b; @@ -15,60 +14,6 @@ use tw_keypair::ed25519; use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; use tw_memory::Data; -/// This enums specifies the intent scope. -#[derive(Serialize_repr)] -#[repr(u8)] -pub enum IntentScope { - /// Used for a user signature on a transaction data. - TransactionData = 0, -} - -/// The version here is to distinguish between signing different versions of the struct -/// or enum. Serialized output between two different versions of the same struct/enum -/// might accidentally (or maliciously on purpose) match. -#[derive(Serialize_repr)] -#[repr(u8)] -pub enum IntentVersion { - V0 = 0, -} - -/// This enums specifies the application ID. Two intents in two different applications -/// (i.e., Narwhal, Sui, Ethereum etc) should never collide, so that even when a signing -/// key is reused, nobody can take a signature designated for app_1 and present it as a -/// valid signature for an (any) intent in app_2. -#[derive(Serialize_repr)] -#[repr(u8)] -pub enum AppId { - Sui = 0, -} - -/// An intent is a compact struct serves as the domain separator for a message that a signature commits to. -/// It consists of three parts: [enum IntentScope] (what the type of the message is), -/// [enum IntentVersion], [enum AppId] (what application that the signature refers to). -/// It is used to construct [struct IntentMessage] that what a signature commits to. -/// -/// The serialization of an Intent is a 3-byte array where each field is represented by a byte. -#[derive(Serialize)] -pub struct Intent { - pub scope: IntentScope, - pub version: IntentVersion, - pub app_id: AppId, -} - -/// Intent Message is a wrapper around a message with its intent. The message can -/// be any type that implements [trait Serialize]. *ALL* signatures in Sui must commits -/// to the intent message, not the message itself. This guarantees any intent -/// message signed in the system cannot collide with another since they are domain -/// separated by intent. -/// -/// The serialization of an IntentMessage is compact: it only appends three bytes -/// to the message itself. -#[derive(Serialize)] -pub struct IntentMessage { - pub intent: Intent, - pub value: T, -} - pub struct TransactionPreimage { /// Transaction `bcs` encoded representation. pub unsigned_tx_data: Data, @@ -116,11 +61,7 @@ impl TxSigner { } pub fn preimage_direct(unsigned_tx_data: Data) -> SigningResult { - let intent = Intent { - scope: IntentScope::TransactionData, - version: IntentVersion::V0, - app_id: AppId::Sui, - }; + let intent = Intent::sui_transaction(); let intent_data = bcs::encode(&intent) .tw_err(|_| SigningErrorType::Error_internal) .context("Error serializing Intent message")?; diff --git a/rust/chains/tw_sui/src/signature.rs b/rust/chains/tw_sui/src/signature.rs index e03aacad05f..61352009c8a 100644 --- a/rust/chains/tw_sui/src/signature.rs +++ b/rust/chains/tw_sui/src/signature.rs @@ -2,9 +2,10 @@ // // Copyright © 2017 Trust Wallet. +use tw_coin_entry::error::prelude::{ResultContext, SigningError, SigningResult}; use tw_encoding::base64::{self, STANDARD}; use tw_hash::{H256, H512}; -use tw_keypair::ed25519; +use tw_keypair::{ed25519, KeyPairError}; use tw_memory::Data; #[derive(Clone, Copy)] @@ -15,8 +16,8 @@ pub enum SignatureScheme { pub struct SuiSignatureInfo { scheme: SignatureScheme, - signature: H512, - public_key: H256, + pub signature: H512, + pub public_key: H256, } impl SuiSignatureInfo { @@ -31,6 +32,33 @@ impl SuiSignatureInfo { } } + pub fn from_base64(encoded: &str) -> SigningResult { + let data = base64::decode(encoded, STANDARD)?; + if data.len() != H512::LEN + H256::LEN + 1 { + return SigningError::err(tw_proto::Common::Proto::SigningError::Error_invalid_params) + .context("Invalid signature length, expected exactly 97 bytes"); + } + let scheme = match data[0] { + 0 => SignatureScheme::ED25519, + _ => { + return SigningError::err( + tw_proto::Common::Proto::SigningError::Error_not_supported, + ) + .context("Unsupported signature scheme") + }, + }; + let signature = + H512::try_from(&data[1..H512::LEN + 1]).map_err(|_| KeyPairError::InvalidSignature)?; + let public_key = + H256::try_from(&data[H512::LEN + 1..]).map_err(|_| KeyPairError::InvalidPublicKey)?; + + Ok(SuiSignatureInfo { + scheme, + signature, + public_key, + }) + } + pub fn to_vec(&self) -> Data { let mut scheme: Data = Vec::with_capacity(H512::LEN + H256::LEN + 1); scheme.push(self.scheme as u8); diff --git a/rust/tw_tests/tests/chains/sui/mod.rs b/rust/tw_tests/tests/chains/sui/mod.rs index c19d11c0bc7..c73d4ffef3a 100644 --- a/rust/tw_tests/tests/chains/sui/mod.rs +++ b/rust/tw_tests/tests/chains/sui/mod.rs @@ -6,6 +6,7 @@ use tw_proto::Sui::Proto; mod sui_address; mod sui_compile; +mod sui_message_sign; mod sui_sign; mod sui_transaction_util; mod test_cases; diff --git a/rust/tw_tests/tests/chains/sui/sui_message_sign.rs b/rust/tw_tests/tests/chains/sui/sui_message_sign.rs new file mode 100644 index 00000000000..b385a6817a8 --- /dev/null +++ b/rust/tw_tests/tests/chains/sui/sui_message_sign.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::ffi::tw_message_signer::{ + tw_message_signer_pre_image_hashes, tw_message_signer_sign, tw_message_signer_verify, +}; +use tw_coin_entry::error::prelude::SigningErrorType; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex; +use tw_encoding::hex::DecodeHex; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_proto::{deserialize, serialize, Sui, TxCompiler}; + +#[test] +fn test_sui_message_signer_sign() { + let input = Sui::Proto::MessageSigningInput { + private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d" + .decode_hex() + .unwrap() + .into(), + message: "Hello world".into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let output = TWDataHelper::wrap(unsafe { + tw_message_signer_sign(CoinType::Sui as u32, input_data.ptr()) + }) + .to_vec() + .expect("!tw_message_signer_sign returned nullptr"); + + let output: Sui::Proto::MessageSigningOutput = deserialize(&output).unwrap(); + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + assert_eq!( + output.signature, + "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q==" + ); +} + +#[test] +fn test_sui_message_signer_verify() { + let input = Sui::Proto::MessageVerifyingInput { + public_key: "ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9" + .decode_hex() + .unwrap() + .into(), + message: "Hello world".into(), + signature: "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q==".into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let verified = unsafe { tw_message_signer_verify(CoinType::Sui as u32, input_data.ptr()) }; + assert!(verified); +} + +#[test] +fn test_sui_message_signer_pre_image_hashes() { + let message = "Hello world"; + + let input = Sui::Proto::MessageSigningInput { + private_key: Default::default(), + message: message.into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let output = TWDataHelper::wrap(unsafe { + tw_message_signer_pre_image_hashes(CoinType::Sui as u32, input_data.ptr()) + }) + .to_vec() + .expect("!tw_message_signer_pre_image_hashes returned nullptr"); + + let output: TxCompiler::Proto::PreSigningOutput = deserialize(&output).unwrap(); + assert_eq!(output.error, SigningErrorType::OK); + assert!(output.error_message.is_empty()); + + let expected = "6b27c39ed22f5346dbce4eca17640e1d139012768746aaa42eafe103f2f9ede2"; + assert_eq!(hex::encode(output.data, false), expected); + assert_eq!(hex::encode(output.data_hash, false), expected); +} diff --git a/src/interface/TWMessageSigner.cpp b/src/interface/TWMessageSigner.cpp index 805c3928e92..4090df8e5e4 100644 --- a/src/interface/TWMessageSigner.cpp +++ b/src/interface/TWMessageSigner.cpp @@ -7,6 +7,19 @@ using namespace TW; +TWData* _Nullable TWMessageSignerPreImageHashes(enum TWCoinType coin, TWData* _Nonnull input) { + const Data& dataIn = *(reinterpret_cast(input)); + + Rust::TWDataWrapper inputData(dataIn); + Rust::TWDataWrapper outputPtr = Rust::tw_message_signer_pre_image_hashes(coin, inputData.get()); + + auto outputData = outputPtr.toDataOrDefault(); + if (outputData.empty()) { + return nullptr; + } + return TWDataCreateWithBytes(outputData.data(), outputData.size()); +} + TWData* _Nullable TWMessageSignerSign(enum TWCoinType coin, TWData* _Nonnull input) { const Data& dataIn = *(reinterpret_cast(input)); diff --git a/src/proto/Sui.proto b/src/proto/Sui.proto index 1efb8798b8c..953835d9ca4 100644 --- a/src/proto/Sui.proto +++ b/src/proto/Sui.proto @@ -152,3 +152,40 @@ message SigningOutput { // Error description. string error_message = 4; } + +// Message signing input. +message MessageSigningInput { + // The secret private key used for signing (32 bytes). + bytes private_key = 1; + + // A UTF-8 regular message to sign. + string message = 2; +} + +// Message signing output. +message MessageSigningOutput { + // The signature, a 97-byte array encoded in base64. + // The first byte indicates the signature scheme (currently set to 0x00, as we only support ED25519). + // The following 64 bytes represent the raw ED25519 signature, while the next 32 bytes contain the public key. + string signature = 1; + + // error code, 0 is ok, other codes will be treated as errors + Common.Proto.SigningError error = 2; + + // error code description + string error_message = 3; +} + +// Message verifying input. +message MessageVerifyingInput { + // The message signed. + string message = 1; + + // Public key that will verify the message. + // It must be equal to the public key encoded in the signature. + bytes public_key = 2; + + // The signature, a 97-byte array encoded in base64. + // Same as the signature field in MessageSigningOutput. + string signature = 3; +} diff --git a/tests/chains/Sui/MessageSignerTests.cpp b/tests/chains/Sui/MessageSignerTests.cpp new file mode 100644 index 00000000000..4f20f397a4a --- /dev/null +++ b/tests/chains/Sui/MessageSignerTests.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include +#include + +#include "Data.h" +#include "HexCoding.h" +#include "proto/Sui.pb.h" +#include "proto/TransactionCompiler.pb.h" + +#include "TestUtilities.h" +#include +#include + +namespace TW::Sui { + +TEST(SuiMessageSigner, Sign) { + const auto privateKey = parse_hex("44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d"); + const auto message = "Hello world"; + + Proto::MessageSigningInput input; + input.set_private_key(privateKey.data(), privateKey.size()); + input.set_message(message); + + const auto inputData = data(input.SerializeAsString()); + const auto inputDataPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + Proto::MessageSigningOutput output; + const auto outputDataPtr = WRAPD(TWMessageSignerSign(TWCoinTypeSui, inputDataPtr.get())); + output.ParseFromArray(TWDataBytes(outputDataPtr.get()), static_cast(TWDataSize(outputDataPtr.get()))); + + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + EXPECT_EQ(output.signature(), "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q=="); +} + +TEST(SuiMessageSigner, Verify) { + const auto publicKey = parse_hex("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9"); + const auto message = "Hello world"; + const auto signature = "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q=="; + + Proto::MessageVerifyingInput input; + input.set_public_key(publicKey.data(), publicKey.size()); + input.set_message(message); + input.set_signature(signature); + + const auto inputData = data(input.SerializeAsString()); + const auto inputDataPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + EXPECT_TRUE(TWMessageSignerVerify(TWCoinTypeSui, inputDataPtr.get())); +} + +TEST(SuiMessageSigner, PreImageHashes) { + const auto publicKey = parse_hex("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9"); + const auto message = "Hello world"; + const auto signature = "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q=="; + + Proto::MessageSigningInput input; + input.set_message(message); + + const auto inputData = data(input.SerializeAsString()); + const auto inputDataPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + const auto outputDataPtr = WRAPD(TWMessageSignerPreImageHashes(TWCoinTypeSui, inputDataPtr.get())); + + TxCompiler::Proto::PreSigningOutput output; + output.ParseFromArray(TWDataBytes(outputDataPtr.get()), static_cast(TWDataSize(outputDataPtr.get()))); + + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + ASSERT_EQ(hex(output.data()), "6b27c39ed22f5346dbce4eca17640e1d139012768746aaa42eafe103f2f9ede2"); + ASSERT_EQ(hex(output.data_hash()), "6b27c39ed22f5346dbce4eca17640e1d139012768746aaa42eafe103f2f9ede2"); +} + +} // namespace TW::Sui From defc6158ae3183f4cca01cb6e23f319e8fcc9f19 Mon Sep 17 00:00:00 2001 From: 10gic <2391796+10gic@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:57:04 +0800 Subject: [PATCH 2/3] Add more unit test cases --- .../tests/chains/sui/sui_message_sign.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rust/tw_tests/tests/chains/sui/sui_message_sign.rs b/rust/tw_tests/tests/chains/sui/sui_message_sign.rs index b385a6817a8..9fd0a93deaa 100644 --- a/rust/tw_tests/tests/chains/sui/sui_message_sign.rs +++ b/rust/tw_tests/tests/chains/sui/sui_message_sign.rs @@ -54,6 +54,22 @@ fn test_sui_message_signer_verify() { assert!(verified); } +#[test] +fn test_sui_message_signer_verify_different_public_key() { + let input = Sui::Proto::MessageVerifyingInput { + public_key: "50af6683c41cd209ad48051ddae8bc588fc56bdbfbce74484768fde68cd93cac" // random public key + .decode_hex() + .unwrap() + .into(), + message: "Hello world".into(), + signature: "ABUNBl59ILPhyGpdgWpXJIQtEIMidR27As1771Hn7j9wVR/5IetQslRPMBrUC2THM+yGHw7h2N/Mr/0DMOpXLQ7ubWGon8j5kJWFqZa7DSsqxpriO1rPOaGfMmMSOboG+Q==".into(), + }; + + let input_data = TWDataHelper::create(serialize(&input).unwrap()); + let verified = unsafe { tw_message_signer_verify(CoinType::Sui as u32, input_data.ptr()) }; + assert_eq!(verified, false); +} + #[test] fn test_sui_message_signer_pre_image_hashes() { let message = "Hello world"; From 6b1c27adbbca6fcc9edd17a6221ff53e22e13c6d Mon Sep 17 00:00:00 2001 From: 10gic <2391796+10gic@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:12:10 +0800 Subject: [PATCH 3/3] Only fix warning --- rust/chains/tw_sui/src/modules/intent.rs | 4 ++-- rust/tw_cosmos_sdk/tests/sign.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/rust/chains/tw_sui/src/modules/intent.rs b/rust/chains/tw_sui/src/modules/intent.rs index 4e62230a236..ec98682c7a9 100644 --- a/rust/chains/tw_sui/src/modules/intent.rs +++ b/rust/chains/tw_sui/src/modules/intent.rs @@ -5,8 +5,8 @@ use serde::Serialize; use serde_repr::Serialize_repr; -/// Code snippets from: -/// https://github.com/MystenLabs/sui/blob/a16c942b72c13f42846b3c543b6622af85a5f634/crates/shared-crypto/src/intent.rs +// Code snippets from: +// https://github.com/MystenLabs/sui/blob/a16c942b72c13f42846b3c543b6622af85a5f634/crates/shared-crypto/src/intent.rs /// This enums specifies the intent scope. #[derive(Serialize_repr)] diff --git a/rust/tw_cosmos_sdk/tests/sign.rs b/rust/tw_cosmos_sdk/tests/sign.rs index ae3c5595f14..2ce4fb61d16 100644 --- a/rust/tw_cosmos_sdk/tests/sign.rs +++ b/rust/tw_cosmos_sdk/tests/sign.rs @@ -289,8 +289,6 @@ fn test_sign_direct() { /// and `AuthInfo` will be generated from `SigningInput` parameters. #[test] fn test_sign_direct_with_body_bytes() { - use tw_cosmos_sdk::proto::cosmos::tx::v1beta1 as tx_proto; - let coin = TestCoinContext::default() .with_public_key_type(PublicKeyType::Secp256k1) .with_hrp("cosmos"); @@ -302,7 +300,7 @@ fn test_sign_direct_with_body_bytes() { // Do not specify the `AuthInfo` bytes. auth_info_bytes: Cow::default(), }; - let mut input = Proto::SigningInput { + let input = Proto::SigningInput { account_number: 1037, chain_id: "gaia-13003".into(), fee: Some(make_fee(200000, make_amount("muon", "200"))),