Skip to content

Commit

Permalink
[Sui]: Support Sui sign personal message (#4223)
Browse files Browse the repository at this point in the history
* Support Sui sign personal message

* Add more unit test cases

* Only fix warning

---------

Co-authored-by: Sergei Boiko <[email protected]>
  • Loading branch information
10gic and satoshiotomakan authored Jan 20, 2025
1 parent 2d7166d commit 48147df
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 69 deletions.
8 changes: 8 additions & 0 deletions include/TrustWalletCore/TWMessageSigner.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions rust/chains/tw_sui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
9 changes: 7 additions & 2 deletions rust/chains/tw_sui/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -92,6 +92,11 @@ impl CoinEntry for SuiEntry {
SuiCompiler::compile(coin, input, signatures, public_keys)
}

#[inline]
fn message_signer(&self) -> Option<Self::MessageSigner> {
Some(SuiMessageSigner)
}

#[inline]
fn transaction_util(&self) -> Option<Self::TransactionUtil> {
Some(SuiTransactionUtil)
Expand Down
95 changes: 95 additions & 0 deletions rust/chains/tw_sui/src/modules/intent.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
pub intent: Intent,
pub value: T,
}

impl<T> IntentMessage<T> {
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<u8>,
}
115 changes: 115 additions & 0 deletions rust/chains/tw_sui/src/modules/message_signer.rs
Original file line number Diff line number Diff line change
@@ -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<Proto::MessageSigningOutput<'static>> {
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<H256> {
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())
}
}
2 changes: 2 additions & 0 deletions rust/chains/tw_sui/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
63 changes: 2 additions & 61 deletions rust/chains/tw_sui/src/modules/tx_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<T> {
pub intent: Intent,
pub value: T,
}

pub struct TransactionPreimage {
/// Transaction `bcs` encoded representation.
pub unsigned_tx_data: Data,
Expand Down Expand Up @@ -116,11 +61,7 @@ impl TxSigner {
}

pub fn preimage_direct(unsigned_tx_data: Data) -> SigningResult<TransactionPreimage> {
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")?;
Expand Down
Loading

0 comments on commit 48147df

Please sign in to comment.