Skip to content

Commit

Permalink
[Solana]: Add Solana custom message signing (#4134)
Browse files Browse the repository at this point in the history
* [Solana]: Add Solana UTF8 message signer

* [Solana]: Add `TWMessageSigner` - a generic custom message signer for any chain

* [Solana]: Add Android, iOS tests

* [Solana]: Minor improvement
  • Loading branch information
satoshiotomakan authored Nov 28, 2024
1 parent 9dae7e1 commit 60ef068
Show file tree
Hide file tree
Showing 16 changed files with 382 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.trustwallet.core.app.blockchains.solana

import com.google.protobuf.ByteString
import com.trustwallet.core.app.utils.toHex
import com.trustwallet.core.app.utils.toHexByteArray
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.Base58
import wallet.core.java.AnySigner
import wallet.core.jni.CoinType.SOLANA
import wallet.core.jni.MessageSigner
import wallet.core.jni.proto.Common.SigningError
import wallet.core.jni.proto.Solana

class TestSolanaMessageSigner {
init {
System.loadLibrary("TrustWalletCore")
}

@Test
fun testMessageSign() {
val signingInput = Solana.MessageSigningInput.newBuilder().apply {
privateKey = ByteString.copyFrom("44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d".toHexByteArray())
message = "Hello world"
}.build()

val outputData = MessageSigner.sign(SOLANA, signingInput.toByteArray())
val output = Solana.MessageSigningOutput.parseFrom(outputData)

assertEquals(output.error, SigningError.OK)
assertEquals(output.signature, "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG")
}

@Test
fun testMessageVerify() {
val verifyingInput = Solana.MessageVerifyingInput.newBuilder().apply {
publicKey = ByteString.copyFrom("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9".toHexByteArray())
signature = "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"
message = "Hello world"
}.build()

assert(MessageSigner.verify(SOLANA, verifyingInput.toByteArray()))
}
}
33 changes: 33 additions & 0 deletions include/TrustWalletCore/TWMessageSigner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

#pragma once

#include "TWBase.h"
#include "TWPrivateKey.h"
#include "TWString.h"

TW_EXTERN_C_BEGIN

/// Represents a message signer to sign custom messages for any blockchain.
TW_EXPORT_CLASS
struct TWMessageSigner;

/// 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.
/// \param input The serialized data of a `MessageSigningInput` proto object, (e.g. `TW.Solana.Proto.MessageSigningInput`).
/// \return The serialized data of a `MessageSigningOutput` proto object, (e.g. `TW.Solana.Proto.MessageSigningOutput`).
TW_EXPORT_STATIC_METHOD
TWData* _Nullable TWMessageSignerSign(enum TWCoinType coin, TWData* _Nonnull input);

/// Verifies a signature for a message.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput).
/// \return whether the signature is valid.
TW_EXPORT_STATIC_METHOD
bool TWMessageSignerVerify(enum TWCoinType coin, TWData* _Nonnull input);

TW_EXTERN_C_END
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/chains/tw_solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,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_solana/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use crate::address::SolanaAddress;
use crate::compiler::SolanaCompiler;
use crate::modules::offchain_message_signer::OffchainMessageSigner;
use crate::modules::transaction_decoder::SolanaTransactionDecoder;
use crate::modules::transaction_util::SolanaTransactionUtil;
use crate::modules::wallet_connect::connector::SolanaWalletConnector;
Expand All @@ -14,7 +15,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::prefix::NoPrefix;
use tw_keypair::tw::PublicKey;
Expand All @@ -32,7 +32,7 @@ impl CoinEntry for SolanaEntry {
// Optional modules:
type JsonSigner = NoJsonSigner;
type PlanBuilder = NoPlanBuilder;
type MessageSigner = NoMessageSigner;
type MessageSigner = OffchainMessageSigner;
type WalletConnector = SolanaWalletConnector;
type TransactionDecoder = SolanaTransactionDecoder;
type TransactionUtil = SolanaTransactionUtil;
Expand Down Expand Up @@ -88,6 +88,11 @@ impl CoinEntry for SolanaEntry {
SolanaCompiler::compile(coin, input, signatures, public_keys)
}

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

#[inline]
fn wallet_connector(&self) -> Option<Self::WalletConnector> {
Some(SolanaWalletConnector)
Expand Down
1 change: 1 addition & 0 deletions rust/chains/tw_solana/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod insert_instruction;
pub mod instruction_builder;
pub mod message_builder;
pub mod message_decompiler;
pub mod offchain_message_signer;
pub mod proto_builder;
pub mod transaction_decoder;
pub mod transaction_util;
Expand Down
74 changes: 74 additions & 0 deletions rust/chains/tw_solana/src/modules/offchain_message_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::SOLANA_ALPHABET;
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::base58;
use tw_keypair::ed25519;
use tw_keypair::traits::{SigningKeyTrait, VerifyingKeyTrait};
use tw_misc::try_or_false;
use tw_proto::Solana::Proto;
use tw_proto::TxCompiler::Proto as CompilerProto;

/// Currently, supports https://solana.com/developers/cookbook/wallets/sign-message only.
pub struct OffchainMessageSigner;

impl OffchainMessageSigner {
pub fn sign_message_impl(
_coin: &dyn CoinContext,
input: Proto::MessageSigningInput,
) -> SigningResult<Proto::MessageSigningOutput<'static>> {
let private_key = ed25519::sha512::PrivateKey::try_from(input.private_key.as_ref())?;
let sign = private_key.sign(input.message.as_bytes().to_vec())?;
let base58_sign = base58::encode(sign.to_bytes().as_slice(), SOLANA_ALPHABET);
Ok(Proto::MessageSigningOutput {
signature: base58_sign.into(),
..Proto::MessageSigningOutput::default()
})
}
}

impl MessageSigner for OffchainMessageSigner {
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 {
CompilerProto::PreSigningOutput {
data: input.message.as_bytes().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 sign = try_or_false!(base58::decode(&input.signature, SOLANA_ALPHABET));
let sign = try_or_false!(ed25519::Signature::try_from(sign.as_slice()));
let public_key = try_or_false!(ed25519::sha512::PublicKey::try_from(
input.public_key.as_ref()
));
let message_utf8 = input.message.as_bytes().to_vec();
public_key.verify(sign, message_utf8)
}
}
18 changes: 9 additions & 9 deletions rust/tw_any_coin/src/ffi/tw_message_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use tw_memory::ffi::tw_data::TWData;
use tw_memory::ffi::RawPtrTrait;
use tw_misc::{try_or_else, try_or_false};

/// Signs a message for the given blockchain.
/// 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.
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput).
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32) -> *mut TWData {
pub unsafe extern "C" fn tw_message_signer_sign(coin: u32, input: *const TWData) -> *mut TWData {
let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut);
let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut);

Expand All @@ -27,25 +27,25 @@ pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32)

/// Verifies a signature for a message.
///
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput).
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput).
/// \return whether the signature is valid.
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_verify(input: *const TWData, coin: u32) -> bool {
pub unsafe extern "C" fn tw_message_signer_verify(coin: u32, input: *const TWData) -> bool {
let input = try_or_false!(TWData::from_ptr_as_ref(input));
let coin = try_or_false!(CoinType::try_from(coin));
MessageSigner::verify_message(input.as_slice(), coin).unwrap_or_default()
}

/// Computes preimage hashes of a message.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of TW.TxCompiler.PreSigningOutput.
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_pre_image_hashes(
input: *const TWData,
coin: u32,
input: *const TWData,
) -> *mut TWData {
let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut);
let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut);
Expand Down
8 changes: 4 additions & 4 deletions rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn test_tw_message_signer_sign() {

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_sign(input_data.ptr(), CoinType::Ethereum as u32)
tw_message_signer_sign(CoinType::Ethereum as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");
Expand All @@ -45,7 +45,7 @@ fn test_tw_message_signer_verify() {
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) };
let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) };
assert!(verified);
}

Expand All @@ -58,7 +58,7 @@ fn test_tw_message_signer_verify_invalid() {
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) };
let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) };
assert!(!verified);
}

Expand All @@ -76,7 +76,7 @@ fn test_tw_message_signer_pre_image_hashes() {

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_pre_image_hashes(input_data.ptr(), CoinType::Ethereum as u32)
tw_message_signer_pre_image_hashes(CoinType::Ethereum as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");
Expand Down
1 change: 1 addition & 0 deletions rust/tw_tests/tests/chains/solana/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod solana_address;
mod solana_address_ffi;
mod solana_compile;
mod solana_message_sign;
mod solana_sign;
mod solana_transaction;
mod solana_transaction_ffi;
Expand Down
80 changes: 80 additions & 0 deletions rust/tw_tests/tests/chains/solana/solana_message_sign.rs
Original file line number Diff line number Diff line change
@@ -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::DecodeHex;
use tw_memory::test_utils::tw_data_helper::TWDataHelper;
use tw_proto::{deserialize, serialize, Solana, TxCompiler};

#[test]
fn test_solana_message_signer_sign() {
let input = Solana::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::Solana as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");

let output: Solana::Proto::MessageSigningOutput = deserialize(&output).unwrap();
assert_eq!(output.error, SigningErrorType::OK);
assert!(output.error_message.is_empty());
assert_eq!(
output.signature,
"2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"
);
}

#[test]
fn test_solana_message_signer_verify() {
let input = Solana::Proto::MessageVerifyingInput {
public_key: "ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9"
.decode_hex()
.unwrap()
.into(),
message: "Hello world".into(),
signature: "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG".into(),
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(CoinType::Solana as u32, input_data.ptr()) };
assert!(verified);
}

#[test]
fn test_solana_message_signer_pre_image_hashes() {
let message = "Hello world";

let input = Solana::Proto::MessageSigningInput {
private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d"
.decode_hex()
.unwrap()
.into(),
message: message.into(),
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_pre_image_hashes(CoinType::Solana as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");

let output: TxCompiler::Proto::PreSigningOutput = deserialize(&output).unwrap();
assert_eq!(output.error, SigningErrorType::OK);
assert!(output.error_message.is_empty());
let actual_message = String::from_utf8(output.data.to_vec()).unwrap();
assert_eq!(actual_message, message);
}
Loading

0 comments on commit 60ef068

Please sign in to comment.