From 8b17ed9597847ec5ebc6e349061c04d3981e4158 Mon Sep 17 00:00:00 2001 From: Radmir Date: Fri, 6 Mar 2026 14:00:10 +0500 Subject: [PATCH 1/3] TON signing: include address, timestamp & hash Refactor TON sign flow to include address and timestamp in the sign-data hash and responses. Added gem_hash dependency and sha256-based sign-data construction (SIGN_DATA_PREFIX + workchain + hash + domain + timestamp + type + payload). TonSignMessageData now carries address and provides build_sign_data_hash; TonSignDataPayload gained encode_for_signing. sign_personal now returns TonSignResult { signature, public_key, timestamp } and callers (chain_signer, gemstone) updated accordingly. Added Address::from_base64_url and base64_to_hex_address helpers and adjusted WalletConnect/validator parsing to include the message "from" address. Tests updated to reflect the new behavior. --- Cargo.lock | 1 + crates/gem_ton/Cargo.toml | 3 +- crates/gem_ton/src/address.rs | 47 ++++++---- crates/gem_ton/src/signer/chain_signer.rs | 4 +- crates/gem_ton/src/signer/mod.rs | 2 +- crates/gem_ton/src/signer/signature.rs | 25 +++--- crates/gem_ton/src/signer/types.rs | 89 +++++++++++++++---- .../src/request_handler/ton.rs | 8 +- crates/gem_wallet_connect/src/validator.rs | 18 +++- gemstone/src/message/signer.rs | 43 +++++---- 10 files changed, 173 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6faba48cd..78f3bacc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3348,6 +3348,7 @@ dependencies = [ "crc", "futures", "gem_client", + "gem_hash", "hex", "num-bigint", "primitives", diff --git a/crates/gem_ton/Cargo.toml b/crates/gem_ton/Cargo.toml index 133e693c7..75c016eb3 100644 --- a/crates/gem_ton/Cargo.toml +++ b/crates/gem_ton/Cargo.toml @@ -11,7 +11,7 @@ rpc = [ "dep:chain_traits", "dep:futures", ] -signer = ["dep:signer"] +signer = ["dep:signer", "dep:gem_hash"] reqwest = ["gem_client/reqwest"] chain_integration_tests = ["rpc", "reqwest", "settings/testkit"] @@ -35,6 +35,7 @@ futures = { workspace = true, optional = true } # Optional signer dependencies signer = { path = "../signer", optional = true } +gem_hash = { path = "../gem_hash", optional = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/crates/gem_ton/src/address.rs b/crates/gem_ton/src/address.rs index 0cfcef4da..72fce81e7 100644 --- a/crates/gem_ton/src/address.rs +++ b/crates/gem_ton/src/address.rs @@ -34,10 +34,32 @@ impl Address { Self { workchain, hash_part } } + pub fn workchain(&self) -> Workchain { + self.workchain + } + pub fn get_hash_part(&self) -> &HashPart { &self.hash_part } + pub fn from_base64_url(base64: &str) -> Result { + use base64::prelude::BASE64_STANDARD_NO_PAD; + + let bytes = BASE64_URL_SAFE_NO_PAD + .decode(base64) + .or_else(|_| BASE64_STANDARD_NO_PAD.decode(base64)) + .map_err(|_| ParseError("Invalid base64".to_string()))?; + + if bytes.len() != 36 { + return Err(ParseError("Invalid base64 address length".to_string())); + } + + let workchain = bytes[1] as i8 as i32; + let hash_part: HashPart = bytes[2..34].try_into().map_err(|_| ParseError("Invalid hash length".to_string()))?; + + Ok(Self { workchain, hash_part }) + } + pub fn from_hex_str(hex_str: S) -> Result where S: AsRef, @@ -73,21 +95,8 @@ pub fn hex_to_base64_address(hex_str: String) -> Result Result> { - use base64::prelude::{BASE64_STANDARD_NO_PAD, BASE64_URL_SAFE_NO_PAD}; - - let bytes = BASE64_URL_SAFE_NO_PAD - .decode(&base64_str) - .or_else(|_| BASE64_STANDARD_NO_PAD.decode(&base64_str)) - .map_err(|_| ParseError("Invalid base64".to_string()))?; - - if bytes.len() != 36 { - return Err(ParseError("Invalid base64 length".to_string()).into()); - } - - let workchain = bytes[1] as i32; - let hash = &bytes[2..34]; - - Ok(format!("{}:{}", workchain, hex::encode(hash))) + let address = Address::from_base64_url(&base64_str)?; + Ok(format!("{}:{}", address.workchain(), hex::encode(address.get_hash_part()))) } impl std::error::Error for ParseError {} @@ -164,6 +173,14 @@ mod tests { assert_eq!(hex, "0:8e874b7ad9bbebbfc48810b8939c98f50580246f19982040dbcb253c4c3daf78"); } + #[test] + fn test_from_base64_url() { + let addr = Address::from_base64_url("UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg").unwrap(); + + assert_eq!(addr.workchain(), 0); + assert_eq!(hex::encode(addr.get_hash_part()), "58d5c54fbb8488af7eaad0cdc759ca8f6ff79fc9555106c1339b037ec0a40347"); + } + #[test] fn test_round_trip_conversion() { let original_hex = "0:0e97797708411c29a3cb1f3f810ef4f83f41d990838f7f93ce7082c4ff9aa026"; diff --git a/crates/gem_ton/src/signer/chain_signer.rs b/crates/gem_ton/src/signer/chain_signer.rs index a240f88ce..d0a84da41 100644 --- a/crates/gem_ton/src/signer/chain_signer.rs +++ b/crates/gem_ton/src/signer/chain_signer.rs @@ -7,8 +7,8 @@ pub struct TonChainSigner; impl ChainSigner for TonChainSigner { fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { - let (signature, _public_key) = sign_personal(message, private_key)?; - Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature)) + let result = sign_personal(message, private_key)?; + Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, result.signature)) } fn sign_transfer(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { diff --git a/crates/gem_ton/src/signer/mod.rs b/crates/gem_ton/src/signer/mod.rs index e2cea077a..92f25f91c 100644 --- a/crates/gem_ton/src/signer/mod.rs +++ b/crates/gem_ton/src/signer/mod.rs @@ -4,4 +4,4 @@ mod types; pub use chain_signer::TonChainSigner; pub use signature::sign_personal; -pub use types::{TonSignDataPayload, TonSignDataResponse, TonSignMessageData}; +pub use types::{TonSignDataPayload, TonSignDataResponse, TonSignMessageData, TonSignResult}; diff --git a/crates/gem_ton/src/signer/signature.rs b/crates/gem_ton/src/signer/signature.rs index 55492f883..3e35b8624 100644 --- a/crates/gem_ton/src/signer/signature.rs +++ b/crates/gem_ton/src/signer/signature.rs @@ -1,13 +1,17 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use primitives::SignerError; use signer::Signer; -use super::types::TonSignMessageData; +use super::types::{TonSignMessageData, TonSignResult}; -pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { +pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result { let ton_data = TonSignMessageData::from_bytes(data)?; - let digest = ton_data.payload.hash(); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let digest = ton_data.build_sign_data_hash(timestamp)?; - Signer::sign_ed25519_with_public_key(&digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string())) + let (signature, public_key) = Signer::sign_ed25519_with_public_key(&digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string()))?; + Ok(TonSignResult { signature, public_key, timestamp }) } #[cfg(test)] @@ -18,21 +22,22 @@ mod tests { #[test] fn test_sign_ton_personal() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string()); let data = ton_data.to_bytes(); - let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); + let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").unwrap(); - let (signature, public_key) = sign_personal(&data, &private_key).expect("signing succeeds"); + let result = sign_personal(&data, &private_key).unwrap(); - assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); - assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + assert_eq!(result.signature.len(), 64); + assert_eq!(result.public_key.len(), 32); + assert!(result.timestamp > 0); } #[test] fn test_sign_ton_personal_rejects_invalid_key() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string()); let data = ton_data.to_bytes(); let result = sign_personal(&data, &[0u8; 16]); diff --git a/crates/gem_ton/src/signer/types.rs b/crates/gem_ton/src/signer/types.rs index e4946982d..e15099d2d 100644 --- a/crates/gem_ton/src/signer/types.rs +++ b/crates/gem_ton/src/signer/types.rs @@ -1,6 +1,13 @@ +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use gem_hash::sha2::sha256; use primitives::SignerError; use serde::{Deserialize, Serialize}; +use crate::address::Address; + +const SIGN_DATA_PREFIX: &[u8] = b"\xff\xffton-connect/sign-data/"; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum TonSignDataPayload { @@ -18,8 +25,12 @@ impl TonSignDataPayload { } } - pub fn hash(&self) -> Vec { - self.data().as_bytes().to_vec() + pub fn encode_for_signing(&self) -> Result<(&str, Vec), SignerError> { + match self { + Self::Text { text } => Ok(("txt", text.as_bytes().to_vec())), + Self::Binary { bytes } => Ok(("bin", BASE64.decode(bytes).map_err(|e| SignerError::InvalidInput(e.to_string()))?)), + Self::Cell { .. } => Err(SignerError::InvalidInput("Cell payload not supported for sign-data".to_string())), + } } } @@ -28,6 +39,7 @@ impl TonSignDataPayload { pub struct TonSignDataResponse { signature: String, public_key: String, + address: String, timestamp: u64, domain: String, payload: TonSignDataPayload, @@ -37,16 +49,17 @@ pub struct TonSignDataResponse { pub struct TonSignMessageData { pub payload: TonSignDataPayload, pub domain: String, + pub address: String, } impl TonSignMessageData { - pub fn new(payload: TonSignDataPayload, domain: String) -> Self { - Self { payload, domain } + pub fn new(payload: TonSignDataPayload, domain: String, address: String) -> Self { + Self { payload, domain, address } } - pub fn from_value(payload: serde_json::Value, domain: String) -> Result { + pub fn from_value(payload: serde_json::Value, domain: String, address: String) -> Result { let payload: TonSignDataPayload = serde_json::from_value(payload).map_err(SignerError::from)?; - Ok(Self { payload, domain }) + Ok(Self { payload, domain, address }) } pub fn from_bytes(data: &[u8]) -> Result { @@ -56,13 +69,39 @@ impl TonSignMessageData { pub fn to_bytes(&self) -> Vec { serde_json::to_vec(self).unwrap_or_default() } + + pub fn build_sign_data_hash(&self, timestamp: u64) -> Result, SignerError> { + let address = Address::from_base64_url(&self.address).map_err(|e| SignerError::InvalidInput(e.to_string()))?; + let domain_bytes = self.domain.as_bytes(); + let (type_prefix, payload_bytes) = self.payload.encode_for_signing()?; + + let mut msg = Vec::new(); + msg.extend_from_slice(SIGN_DATA_PREFIX); + msg.extend_from_slice(&address.workchain().to_be_bytes()); + msg.extend_from_slice(address.get_hash_part()); + msg.extend_from_slice(&(domain_bytes.len() as u32).to_be_bytes()); + msg.extend_from_slice(domain_bytes); + msg.extend_from_slice(×tamp.to_be_bytes()); + msg.extend_from_slice(type_prefix.as_bytes()); + msg.extend_from_slice(&(payload_bytes.len() as u32).to_be_bytes()); + msg.extend_from_slice(&payload_bytes); + + Ok(sha256(&msg).to_vec()) + } +} + +pub struct TonSignResult { + pub signature: Vec, + pub public_key: Vec, + pub timestamp: u64, } impl TonSignDataResponse { - pub fn new(signature: String, public_key: String, timestamp: u64, domain: String, payload: TonSignDataPayload) -> Self { + pub fn new(signature: String, public_key: String, address: String, timestamp: u64, domain: String, payload: TonSignDataPayload) -> Self { Self { signature, public_key, + address, timestamp, domain, payload, @@ -78,13 +117,14 @@ impl TonSignDataResponse { mod tests { use super::*; + const TEST_ADDRESS: &str = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"; + #[test] fn test_parse_payload_text() { let json = r#"{"type":"text","text":"Hello TON"}"#; let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Text { text: "Hello TON".to_string() }); - assert_eq!(b"Hello TON".to_vec(), parsed.hash()); } #[test] @@ -93,7 +133,6 @@ mod tests { let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }); - assert_eq!("SGVsbG8=".as_bytes().to_vec(), parsed.hash()); } #[test] @@ -102,20 +141,27 @@ mod tests { let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Cell { cell: "te6c".to_string() }); - assert_eq!("te6c".as_bytes().to_vec(), parsed.hash()); } #[test] fn test_response_to_json() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let response = TonSignDataResponse::new("c2lnbmF0dXJl".to_string(), "cHVibGljS2V5".to_string(), 1234567890, "example.com".to_string(), payload); + let response = TonSignDataResponse::new( + "c2lnbmF0dXJl".to_string(), + "abcdef01".to_string(), + "0:58d5c54fbb8488af7eaad0cdc759ca8f6ff79fc9555106c1339b037ec0a40347".to_string(), + 1234567890, + "example.com".to_string(), + payload, + ); let json = response.to_json().unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(parsed["signature"], "c2lnbmF0dXJl"); - assert_eq!(parsed["publicKey"], "cHVibGljS2V5"); + assert_eq!(parsed["publicKey"], "abcdef01"); + assert_eq!(parsed["address"], "0:58d5c54fbb8488af7eaad0cdc759ca8f6ff79fc9555106c1339b037ec0a40347"); assert_eq!(parsed["timestamp"], 1234567890); assert_eq!(parsed["domain"], "example.com"); assert_eq!(parsed["payload"]["type"], "text"); @@ -125,20 +171,31 @@ mod tests { #[test] fn test_ton_sign_message_data() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let data = TonSignMessageData::new(payload.clone(), "example.com".to_string()); + let data = TonSignMessageData::new(payload.clone(), "example.com".to_string(), TEST_ADDRESS.to_string()); let bytes = data.to_bytes(); let parsed = TonSignMessageData::from_bytes(&bytes).unwrap(); assert_eq!(parsed.payload, payload); assert_eq!(parsed.domain, "example.com"); + assert_eq!(parsed.address, TEST_ADDRESS); } #[test] - fn test_ton_sign_message_data_get_payload() { + fn test_build_sign_data_hash() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let data = TonSignMessageData::new(payload, "example.com".to_string()); + let data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); + + let hash = data.build_sign_data_hash(1234567890).unwrap(); + + assert_eq!(hash.len(), 32); + } + + #[test] + fn test_build_sign_data_hash_cell_unsupported() { + let payload = TonSignDataPayload::Cell { cell: "te6c".to_string() }; + let data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); - assert_eq!(data.payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); + assert!(data.build_sign_data_hash(1234567890).is_err()); } } diff --git a/crates/gem_wallet_connect/src/request_handler/ton.rs b/crates/gem_wallet_connect/src/request_handler/ton.rs index aeea26564..51f8d7173 100644 --- a/crates/gem_wallet_connect/src/request_handler/ton.rs +++ b/crates/gem_wallet_connect/src/request_handler/ton.rs @@ -13,8 +13,9 @@ fn extract_host(url: &str) -> String { impl TonRequestHandler { pub fn parse_sign_message(_chain: Chain, params: Value, domain: &str) -> Result { let payload = params.at(0)?.clone(); + let from = payload.get_value("from")?.string()?.to_string(); let host = extract_host(domain); - let ton_data = TonSignMessageData::from_value(payload, host).map_err(|e| e.to_string())?; + let ton_data = TonSignMessageData::from_value(payload, host, from).map_err(|e| e.to_string())?; let data = String::from_utf8(ton_data.to_bytes()).map_err(|e| format!("Failed to encode TonSignMessageData: {}", e))?; Ok(WalletConnectAction::SignMessage { chain: Chain::Ton, @@ -54,7 +55,7 @@ mod tests { #[test] fn test_parse_sign_message() { - let params = serde_json::from_str(r#"[{"type":"text","text":"Hello TON"}]"#).unwrap(); + let params = serde_json::from_str(r#"[{"type":"text","text":"Hello TON","from":"UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}]"#).unwrap(); let action = TonRequestHandler::parse_sign_message(Chain::Ton, params, "https://react-app.walletconnect.com").unwrap(); let WalletConnectAction::SignMessage { chain, sign_type, data } = action else { panic!("Expected SignMessage action") @@ -64,12 +65,13 @@ mod tests { let parsed: TonSignMessageData = serde_json::from_str(&data).unwrap(); assert_eq!(parsed.domain, "react-app.walletconnect.com"); + assert_eq!(parsed.address, "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"); assert_eq!(parsed.payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); } #[test] fn test_parse_sign_message_extracts_host() { - let params = serde_json::from_str(r#"[{"type":"text","text":"Test"}]"#).unwrap(); + let params = serde_json::from_str(r#"[{"type":"text","text":"Test","from":"UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}]"#).unwrap(); let action = TonRequestHandler::parse_sign_message(Chain::Ton, params, "https://example.com/path?query=1").unwrap(); let WalletConnectAction::SignMessage { data, .. } = action else { panic!("Expected SignMessage action") diff --git a/crates/gem_wallet_connect/src/validator.rs b/crates/gem_wallet_connect/src/validator.rs index 6d485c3cb..beed258ce 100644 --- a/crates/gem_wallet_connect/src/validator.rs +++ b/crates/gem_wallet_connect/src/validator.rs @@ -176,7 +176,11 @@ mod tests { ); // Valid: text payload - let ton_data = TonSignMessageData::new(TonSignDataPayload::Text { text: "Hello".to_string() }, "example.com".to_string()); + let ton_data = TonSignMessageData::new( + TonSignDataPayload::Text { text: "Hello".to_string() }, + "example.com".to_string(), + "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), + ); assert!( validate_sign_message(&sign_validation( Chain::Ton, @@ -188,7 +192,11 @@ mod tests { ); // Valid: binary payload - let ton_data = TonSignMessageData::new(TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }, "example.com".to_string()); + let ton_data = TonSignMessageData::new( + TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }, + "example.com".to_string(), + "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), + ); assert!( validate_sign_message(&sign_validation( Chain::Ton, @@ -200,7 +208,11 @@ mod tests { ); // Valid: cell payload - let ton_data = TonSignMessageData::new(TonSignDataPayload::Cell { cell: "te6c".to_string() }, "example.com".to_string()); + let ton_data = TonSignMessageData::new( + TonSignDataPayload::Cell { cell: "te6c".to_string() }, + "example.com".to_string(), + "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), + ); assert!( validate_sign_message(&sign_validation( Chain::Ton, diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index f7d726faf..2b0a59f37 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -5,7 +5,8 @@ use base64::engine::general_purpose::STANDARD as BASE64; use bs58; use gem_evm::{ETHEREUM_RECOVERY_ID_OFFSET, RECOVERY_ID_INDEX, SIGNATURE_LENGTH, message::eip191_hash_message}; use gem_sui::signer as sui_signer; -use gem_ton::signer::{TonSignDataResponse, TonSignMessageData, sign_personal as ton_sign_personal}; +use gem_ton::address::base64_to_hex_address; +use gem_ton::signer::{TonSignDataResponse, TonSignMessageData, TonSignResult, sign_personal as ton_sign_personal}; use primitives::hex::encode_with_0x; use signer::{SignatureScheme, Signer, hash_eip712}; use sui_types::PersonalMessage; @@ -113,7 +114,8 @@ impl MessageSigner { SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone())?; let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - Ok(ton_data.payload.hash()) + let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + Ok(ton_data.build_sign_data_hash(timestamp)?) } SignDigestType::TronPersonal => Ok(tron_hash_message(&self.message.data).to_vec()), SignDigestType::Eip191 | SignDigestType::Siwe => Ok(eip191_hash_message(&self.message.data).to_vec()), @@ -148,25 +150,14 @@ impl MessageSigner { } } - pub fn get_ton_result(&self, signature: &[u8], public_key: &[u8]) -> Result { - let string = String::from_utf8(self.message.data.clone())?; - let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let payload = ton_data.payload; - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); - - let response = TonSignDataResponse::new(BASE64.encode(signature), BASE64.encode(public_key), timestamp, ton_data.domain, payload); - - Ok(response.to_json()?) - } - pub fn sign(&self, private_key: Vec) -> Result { let private_key = Zeroizing::new(private_key); let hash = self.hash()?; match &self.message.sign_type { SignDigestType::SuiPersonal => sui_signer::sign_digest(&hash, &private_key).map_err(GemstoneError::from), SignDigestType::TonPersonal => { - let (signature, public_key) = ton_sign_personal(&self.message.data, &private_key)?; - self.get_ton_result(&signature, &public_key) + let result = ton_sign_personal(&self.message.data, &private_key)?; + self.get_ton_result(&result) } SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe | SignDigestType::TronPersonal => { let signed = Signer::sign_digest(SignatureScheme::Secp256k1, hash, private_key.to_vec())?; @@ -181,6 +172,25 @@ impl MessageSigner { } } +impl MessageSigner { + fn get_ton_result(&self, result: &TonSignResult) -> Result { + let string = String::from_utf8(self.message.data.clone())?; + let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; + let raw_address = base64_to_hex_address(ton_data.address.clone())?; + + let response = TonSignDataResponse::new( + BASE64.encode(&result.signature), + hex::encode(&result.public_key), + raw_address, + result.timestamp, + ton_data.domain, + ton_data.payload, + ); + + Ok(response.to_json()?) + } +} + #[cfg(test)] mod tests { use super::*; @@ -492,8 +502,9 @@ mod tests { #[test] fn test_ton_personal_preview() { let ton_data = TonSignMessageData::from_value( - serde_json::json!({"type": "text", "text": "Hello TON", "from": "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}), + serde_json::json!({"type": "text", "text": "Hello TON"}), "example.com".to_string(), + "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), ) .unwrap(); let data = String::from_utf8(ton_data.to_bytes()).unwrap(); From 49a7dc673a192aaec4b167450d46d42f8f3570b8 Mon Sep 17 00:00:00 2001 From: Radmir Date: Fri, 6 Mar 2026 17:01:58 +0500 Subject: [PATCH 2/3] Add timestamped TON signing and refactor sign API Introduce explicit timestamp support for TON personal signatures and refactor related APIs. sign_personal now accepts a timestamp and TonSignMessageData::build_sign_data_hash/encode_for_signing were renamed to hash/encode respectively. The timestamp is produced in ChainSigner and MessageSigner (via a new current_timestamp helper) and threaded through signing calls. Address base64 decoding now falls back to standard no-pad, and tests were updated to assert deterministic signature/public key outputs. --- crates/gem_ton/src/address.rs | 4 +--- crates/gem_ton/src/signer/chain_signer.rs | 5 ++++- crates/gem_ton/src/signer/signature.rs | 21 +++++++++++---------- crates/gem_ton/src/signer/types.rs | 10 +++++----- gemstone/src/message/signer.rs | 12 +++++++++--- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/gem_ton/src/address.rs b/crates/gem_ton/src/address.rs index 72fce81e7..65925e101 100644 --- a/crates/gem_ton/src/address.rs +++ b/crates/gem_ton/src/address.rs @@ -1,4 +1,4 @@ -use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine}; +use base64::prelude::{BASE64_STANDARD_NO_PAD, BASE64_URL_SAFE_NO_PAD, Engine}; use crc::Crc; type Workchain = i32; @@ -43,8 +43,6 @@ impl Address { } pub fn from_base64_url(base64: &str) -> Result { - use base64::prelude::BASE64_STANDARD_NO_PAD; - let bytes = BASE64_URL_SAFE_NO_PAD .decode(base64) .or_else(|_| BASE64_STANDARD_NO_PAD.decode(base64)) diff --git a/crates/gem_ton/src/signer/chain_signer.rs b/crates/gem_ton/src/signer/chain_signer.rs index d0a84da41..028dad7e4 100644 --- a/crates/gem_ton/src/signer/chain_signer.rs +++ b/crates/gem_ton/src/signer/chain_signer.rs @@ -1,3 +1,5 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use primitives::{ChainSigner, SignerError, TransactionLoadInput}; use super::signature::sign_personal; @@ -7,7 +9,8 @@ pub struct TonChainSigner; impl ChainSigner for TonChainSigner { fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { - let result = sign_personal(message, private_key)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let result = sign_personal(message, private_key, timestamp)?; Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, result.signature)) } diff --git a/crates/gem_ton/src/signer/signature.rs b/crates/gem_ton/src/signer/signature.rs index 3e35b8624..720ea04a1 100644 --- a/crates/gem_ton/src/signer/signature.rs +++ b/crates/gem_ton/src/signer/signature.rs @@ -1,14 +1,11 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - use primitives::SignerError; use signer::Signer; use super::types::{TonSignMessageData, TonSignResult}; -pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result { +pub fn sign_personal(data: &[u8], private_key: &[u8], timestamp: u64) -> Result { let ton_data = TonSignMessageData::from_bytes(data)?; - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); - let digest = ton_data.build_sign_data_hash(timestamp)?; + let digest = ton_data.hash(timestamp)?; let (signature, public_key) = Signer::sign_ed25519_with_public_key(&digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string()))?; Ok(TonSignResult { signature, public_key, timestamp }) @@ -26,12 +23,16 @@ mod tests { let data = ton_data.to_bytes(); let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").unwrap(); + let timestamp = 1234567890u64; - let result = sign_personal(&data, &private_key).unwrap(); + let result = sign_personal(&data, &private_key, timestamp).unwrap(); - assert_eq!(result.signature.len(), 64); - assert_eq!(result.public_key.len(), 32); - assert!(result.timestamp > 0); + assert_eq!( + hex::encode(&result.signature), + "3fe42db1d77534ba52d43240cf6b84b36eb1c53a28e3370c5872f37558cee9b758b9f93a8740c84ee4190b99de83901dcb9d5b42b1c7826b3836236ef5cd3a0f" + ); + assert_eq!(hex::encode(&result.public_key), "d369452197c2a56481e5e2d3e8bf03de2349f67a63151956822208c2334adee2"); + assert_eq!(result.timestamp, timestamp); } #[test] @@ -40,7 +41,7 @@ mod tests { let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string()); let data = ton_data.to_bytes(); - let result = sign_personal(&data, &[0u8; 16]); + let result = sign_personal(&data, &[0u8; 16], 1234567890); assert!(result.is_err()); } } diff --git a/crates/gem_ton/src/signer/types.rs b/crates/gem_ton/src/signer/types.rs index e15099d2d..f9a9c1834 100644 --- a/crates/gem_ton/src/signer/types.rs +++ b/crates/gem_ton/src/signer/types.rs @@ -25,7 +25,7 @@ impl TonSignDataPayload { } } - pub fn encode_for_signing(&self) -> Result<(&str, Vec), SignerError> { + pub fn encode(&self) -> Result<(&str, Vec), SignerError> { match self { Self::Text { text } => Ok(("txt", text.as_bytes().to_vec())), Self::Binary { bytes } => Ok(("bin", BASE64.decode(bytes).map_err(|e| SignerError::InvalidInput(e.to_string()))?)), @@ -70,10 +70,10 @@ impl TonSignMessageData { serde_json::to_vec(self).unwrap_or_default() } - pub fn build_sign_data_hash(&self, timestamp: u64) -> Result, SignerError> { + pub fn hash(&self, timestamp: u64) -> Result, SignerError> { let address = Address::from_base64_url(&self.address).map_err(|e| SignerError::InvalidInput(e.to_string()))?; let domain_bytes = self.domain.as_bytes(); - let (type_prefix, payload_bytes) = self.payload.encode_for_signing()?; + let (type_prefix, payload_bytes) = self.payload.encode()?; let mut msg = Vec::new(); msg.extend_from_slice(SIGN_DATA_PREFIX); @@ -186,7 +186,7 @@ mod tests { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); - let hash = data.build_sign_data_hash(1234567890).unwrap(); + let hash = data.hash(1234567890).unwrap(); assert_eq!(hash.len(), 32); } @@ -196,6 +196,6 @@ mod tests { let payload = TonSignDataPayload::Cell { cell: "te6c".to_string() }; let data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); - assert!(data.build_sign_data_hash(1234567890).is_err()); + assert!(data.hash(1234567890).is_err()); } } diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index 2b0a59f37..14d8651b3 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -9,6 +9,7 @@ use gem_ton::address::base64_to_hex_address; use gem_ton::signer::{TonSignDataResponse, TonSignMessageData, TonSignResult, sign_personal as ton_sign_personal}; use primitives::hex::encode_with_0x; use signer::{SignatureScheme, Signer, hash_eip712}; +use std::time::{SystemTime, UNIX_EPOCH}; use sui_types::PersonalMessage; use super::{ @@ -20,6 +21,10 @@ use gem_bitcoin::signer::{BitcoinSignMessageData, sign_personal as bitcoin_sign_ use gem_tron::signer::tron_hash_message; use zeroize::Zeroizing; +fn current_timestamp() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) +} + #[derive(Debug, PartialEq, uniffi::Enum)] pub enum MessagePreview { Text(String), @@ -114,8 +119,8 @@ impl MessageSigner { SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone())?; let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); - Ok(ton_data.build_sign_data_hash(timestamp)?) + let timestamp = current_timestamp(); + Ok(ton_data.hash(timestamp)?) } SignDigestType::TronPersonal => Ok(tron_hash_message(&self.message.data).to_vec()), SignDigestType::Eip191 | SignDigestType::Siwe => Ok(eip191_hash_message(&self.message.data).to_vec()), @@ -156,7 +161,8 @@ impl MessageSigner { match &self.message.sign_type { SignDigestType::SuiPersonal => sui_signer::sign_digest(&hash, &private_key).map_err(GemstoneError::from), SignDigestType::TonPersonal => { - let result = ton_sign_personal(&self.message.data, &private_key)?; + let timestamp = current_timestamp(); + let result = ton_sign_personal(&self.message.data, &private_key, timestamp)?; self.get_ton_result(&result) } SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe | SignDigestType::TronPersonal => { From a548911f71e1039f6c42da85a23ce1d38272fa34 Mon Sep 17 00:00:00 2001 From: Radmir Date: Fri, 6 Mar 2026 22:29:01 +0500 Subject: [PATCH 3/3] Address review comments: propagate timestamp errors, extract test constant --- crates/gem_ton/src/signer/chain_signer.rs | 5 ++++- crates/gem_ton/src/signer/mod.rs | 2 ++ crates/gem_ton/src/signer/signature.rs | 5 +++-- crates/gem_ton/src/signer/testkit.rs | 1 + crates/gem_ton/src/signer/types.rs | 3 +-- gemstone/src/message/signer.rs | 19 +++++++++++-------- 6 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 crates/gem_ton/src/signer/testkit.rs diff --git a/crates/gem_ton/src/signer/chain_signer.rs b/crates/gem_ton/src/signer/chain_signer.rs index 028dad7e4..496a438f3 100644 --- a/crates/gem_ton/src/signer/chain_signer.rs +++ b/crates/gem_ton/src/signer/chain_signer.rs @@ -9,7 +9,10 @@ pub struct TonChainSigner; impl ChainSigner for TonChainSigner { fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| SignerError::InvalidInput(e.to_string()))? + .as_secs(); let result = sign_personal(message, private_key, timestamp)?; Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, result.signature)) } diff --git a/crates/gem_ton/src/signer/mod.rs b/crates/gem_ton/src/signer/mod.rs index 92f25f91c..a0a7b3ded 100644 --- a/crates/gem_ton/src/signer/mod.rs +++ b/crates/gem_ton/src/signer/mod.rs @@ -1,5 +1,7 @@ mod chain_signer; mod signature; +#[cfg(test)] +pub(crate) mod testkit; mod types; pub use chain_signer::TonChainSigner; diff --git a/crates/gem_ton/src/signer/signature.rs b/crates/gem_ton/src/signer/signature.rs index 720ea04a1..3abb03baa 100644 --- a/crates/gem_ton/src/signer/signature.rs +++ b/crates/gem_ton/src/signer/signature.rs @@ -15,11 +15,12 @@ pub fn sign_personal(data: &[u8], private_key: &[u8], timestamp: u64) -> Result< mod tests { use super::*; use crate::signer::TonSignDataPayload; + use crate::signer::testkit::TEST_ADDRESS; #[test] fn test_sign_ton_personal() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string()); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); let data = ton_data.to_bytes(); let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").unwrap(); @@ -38,7 +39,7 @@ mod tests { #[test] fn test_sign_ton_personal_rejects_invalid_key() { let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; - let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string()); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string(), TEST_ADDRESS.to_string()); let data = ton_data.to_bytes(); let result = sign_personal(&data, &[0u8; 16], 1234567890); diff --git a/crates/gem_ton/src/signer/testkit.rs b/crates/gem_ton/src/signer/testkit.rs new file mode 100644 index 000000000..f71818526 --- /dev/null +++ b/crates/gem_ton/src/signer/testkit.rs @@ -0,0 +1 @@ +pub const TEST_ADDRESS: &str = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"; diff --git a/crates/gem_ton/src/signer/types.rs b/crates/gem_ton/src/signer/types.rs index f9a9c1834..ef1933cfe 100644 --- a/crates/gem_ton/src/signer/types.rs +++ b/crates/gem_ton/src/signer/types.rs @@ -116,8 +116,7 @@ impl TonSignDataResponse { #[cfg(test)] mod tests { use super::*; - - const TEST_ADDRESS: &str = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"; + use crate::signer::testkit::TEST_ADDRESS; #[test] fn test_parse_payload_text() { diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index 14d8651b3..5825b2d5e 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -21,8 +21,11 @@ use gem_bitcoin::signer::{BitcoinSignMessageData, sign_personal as bitcoin_sign_ use gem_tron::signer::tron_hash_message; use zeroize::Zeroizing; -fn current_timestamp() -> u64 { - SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) +fn current_timestamp() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .map_err(|e| GemstoneError::from(e.to_string())) } #[derive(Debug, PartialEq, uniffi::Enum)] @@ -119,7 +122,7 @@ impl MessageSigner { SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone())?; let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let timestamp = current_timestamp(); + let timestamp = current_timestamp()?; Ok(ton_data.hash(timestamp)?) } SignDigestType::TronPersonal => Ok(tron_hash_message(&self.message.data).to_vec()), @@ -161,7 +164,7 @@ impl MessageSigner { match &self.message.sign_type { SignDigestType::SuiPersonal => sui_signer::sign_digest(&hash, &private_key).map_err(GemstoneError::from), SignDigestType::TonPersonal => { - let timestamp = current_timestamp(); + let timestamp = current_timestamp()?; let result = ton_sign_personal(&self.message.data, &private_key, timestamp)?; self.get_ton_result(&result) } @@ -181,16 +184,16 @@ impl MessageSigner { impl MessageSigner { fn get_ton_result(&self, result: &TonSignResult) -> Result { let string = String::from_utf8(self.message.data.clone())?; - let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let raw_address = base64_to_hex_address(ton_data.address.clone())?; + let data = TonSignMessageData::from_bytes(string.as_bytes())?; + let raw_address = base64_to_hex_address(data.address.clone())?; let response = TonSignDataResponse::new( BASE64.encode(&result.signature), hex::encode(&result.public_key), raw_address, result.timestamp, - ton_data.domain, - ton_data.payload, + data.domain, + data.payload, ); Ok(response.to_json()?)