Skip to content

TON signing: include address, timestamp & hash#991

Merged
DRadmir merged 3 commits intomainfrom
ton-walletconnect-fix
Mar 6, 2026
Merged

TON signing: include address, timestamp & hash#991
DRadmir merged 3 commits intomainfrom
ton-walletconnect-fix

Conversation

@DRadmir
Copy link
Contributor

@DRadmir DRadmir commented Mar 6, 2026

WalletConnect updated TON signData verification https://github.com/reown-com/web-examples/pull/1006/changes to validate the structured hash per TON Connect spec instead of accepting raw bytes. Without this change, TON personal_sign requests via WalletConnect fail validation on dApp side.

Changes

  • Build sign-data hash per spec: sha256(0xffff + "ton-connect/sign-data/" + address + domain + timestamp +
    payload)
  • Include address and timestamp in sign response
  • Parse from address from WalletConnect payload
  • Add Address::from_base64_url and workchain() helpers

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.
@semanticdiff-com
Copy link

semanticdiff-com bot commented Mar 6, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  crates/gem_wallet_connect/src/validator.rs  84% smaller
  crates/gem_ton/src/signer/chain_signer.rs  38% smaller
  crates/gem_ton/src/signer/signature.rs  28% smaller
  crates/gem_ton/src/signer/types.rs  22% smaller
  crates/gem_ton/src/address.rs  21% smaller
  gemstone/src/message/signer.rs  20% smaller
  Cargo.lock Unsupported file format
  crates/gem_ton/Cargo.toml Unsupported file format
  crates/gem_ton/src/signer/mod.rs  0% smaller
  crates/gem_ton/src/signer/testkit.rs  0% smaller
  crates/gem_wallet_connect/src/request_handler/ton.rs  0% smaller

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the TON signing mechanism by embedding crucial contextual information like the sender's address and the signing timestamp directly into the data that gets hashed and signed. This change enhances the integrity and non-repudiation of TON transactions and messages, making them more secure and auditable. The refactoring touches core signing logic, data structures, and integrates new hashing capabilities, ensuring that all components correctly handle the enriched signing data.

Highlights

  • Enhanced TON Signing Security: The TON sign flow has been refactored to include the address and timestamp directly within the signed data hash, significantly improving the security and traceability of signing operations.
  • New SHA256-based Sign-Data Construction: A new SHA256-based method for constructing sign-data has been implemented, incorporating a specific prefix, workchain, hash, domain, timestamp, type, and payload, ensuring a robust and standardized signing process.
  • Updated Signer Output and Data Structures: The sign_personal function now returns a TonSignResult struct containing the signature, public key, and timestamp. Corresponding data structures like TonSignMessageData and TonSignDataResponse have been updated to carry the address and timestamp, and TonSignDataPayload gained an encode_for_signing method.
  • Address Utility Functions: New utility functions, Address::from_base64_url and base64_to_hex_address, have been added to handle TON address conversions, improving flexibility in address representation.
  • WalletConnect and Validator Integration: WalletConnect and validator parsing logic has been adjusted to correctly include and process the 'from' address in signing requests, ensuring end-to-end consistency with the new signing flow.
  • Dependency Update: The gem_hash dependency has been introduced to support the new SHA256 hashing requirements.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • Cargo.lock
    • Added the gem_hash dependency.
  • crates/gem_ton/Cargo.toml
    • Added gem_hash as an optional dependency for the signer feature.
  • crates/gem_ton/src/address.rs
    • Added a workchain getter method to the Address struct.
    • Implemented from_base64_url to parse TON addresses from base64 URL format.
    • Refactored base64_to_hex_address to utilize the new Address::from_base64_url method.
    • Added a new test case for from_base64_url.
  • crates/gem_ton/src/signer/chain_signer.rs
    • Updated the sign_message function to correctly process the new TonSignResult structure returned by sign_personal.
  • crates/gem_ton/src/signer/mod.rs
    • Exported the newly introduced TonSignResult struct.
  • crates/gem_ton/src/signer/signature.rs
    • Modified sign_personal to include the current timestamp in the data digest calculation.
    • Changed sign_personal's return type from a tuple to the new TonSignResult struct.
    • Updated test cases to align with the new TonSignResult return type and the TonSignMessageData constructor.
  • crates/gem_ton/src/signer/types.rs
    • Imported necessary modules for base64 encoding, SHA256 hashing, and address handling.
    • Defined SIGN_DATA_PREFIX for consistent sign-data construction.
    • Added encode_for_signing method to TonSignDataPayload to prepare payload data for hashing.
    • Included an address field in both TonSignDataResponse and TonSignMessageData structs.
    • Implemented build_sign_data_hash in TonSignMessageData to construct the SHA256 digest using address, domain, timestamp, and payload.
    • Introduced the TonSignResult struct to encapsulate signature, public key, and timestamp.
    • Updated the TonSignDataResponse::new constructor to accept the new address parameter.
    • Adjusted test cases to remove obsolete hash() calls and update TonSignMessageData instantiations with the new address parameter.
  • crates/gem_wallet_connect/src/request_handler/ton.rs
    • Modified parse_sign_message to extract the 'from' address from the WalletConnect payload.
    • Updated TonSignMessageData::from_value calls to pass the extracted 'from' address.
    • Adjusted test cases to include the 'from' address in the JSON payload for sign message requests.
  • crates/gem_wallet_connect/src/validator.rs
    • Updated TonSignMessageData::new calls within test cases to provide the required 'from' address.
  • gemstone/src/message/signer.rs
    • Imported base64_to_hex_address and TonSignResult for TON signing logic.
    • Modified the hash() method for SignDigestType::TonPersonal to use TonSignMessageData::build_sign_data_hash with a timestamp.
    • Refactored the get_ton_result method to accept a TonSignResult and include the address in the TonSignDataResponse.
    • Updated the sign() method to correctly handle the new TonSignResult returned by ton_sign_personal.
    • Adjusted the test_ton_personal_preview test to match the updated TonSignMessageData::from_value signature.
Activity
  • No human activity (comments, reviews) was detected on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request significantly refactors the TON signing process to enhance security by incorporating the sender's address and a timestamp into the signed data, and correctly fixes the workchain sign-extension for masterchain addresses. However, a critical security vulnerability has been identified: the new TON address parsing logic in Address::from_base64_url lacks CRC16 checksum validation, which could lead to processing malformed addresses or typos, potentially resulting in unintended operations or loss of funds. Additionally, a critical issue exists in gemstone/src/message/signer.rs regarding potential timestamp mismatches between hash preview and the actual signed hash, along with medium-severity issues related to error handling of system time and ambiguity in base64 decoding.

Comment on lines 158 to 161
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

There's a critical issue with how the hash for TON signing is handled. The sign method calls self.hash()? at the beginning, which for TonPersonal computes a hash with a timestamp. However, this branch for TonPersonal ignores that computed hash and instead calls ton_sign_personal. This function re-parses the message and computes a new hash with a new timestamp.

This means the hash computed by self.hash() (e.g., for a preview) will not match the hash that is actually signed. This violates the what-you-see-is-what-you-sign principle.

To fix this, the TonPersonal branch should use the hash variable computed at the start of the sign method, just like other signing types. This will require a refactoring to ensure the timestamp is consistent between hash() and sign().

Comment on lines +45 to +61
pub fn from_base64_url(base64: &str) -> Result<Self, ParseError> {
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 })
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The Address::from_base64_url function decodes a base64-encoded TON address but critically fails to validate the 2-byte CRC checksum, which is a standard part of the 36-byte TON user-friendly address format. This omission allows the application to accept malformed addresses or addresses with typos, potentially leading to signing messages for unintended accounts or displaying incorrect information. Beyond this, the function's name from_base64_url suggests it only handles URL-safe base64, but it falls back to decoding standard base64 if URL-safe decoding fails. This behavior, while robust, might obscure issues with data sources expected to provide URL-safe base64. If both formats are intentionally supported, this should be clearly documented; otherwise, the fallback should be removed to enforce the correct format. Remediation: Implement CRC16-CCITT validation to verify that the last 2 bytes of the decoded 36-byte array match the CRC16-CCITT checksum of the first 34 bytes.

pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result<TonSignResult, SignerError> {
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using unwrap_or(0) can hide issues with the system clock. If the clock is set to a time before the UNIX epoch, duration_since will return an error, and this code will produce a timestamp of 0. This could lead to unexpected behavior or signature rejections by servers that validate the timestamp. It would be more robust to handle the error explicitly and return a SignerError.

Suggested change
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 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using unwrap_or(0) can hide issues with the system clock. If the clock is set to a time before the UNIX epoch, duration_since will return an error, and this code will produce a timestamp of 0. This could lead to unexpected behavior or signature rejections by servers that validate the timestamp. It would be more robust to handle the error explicitly and return a GemstoneError.

Suggested change
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(|e| GemstoneError::from(e.to_string()))?.as_secs();

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)?)
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this timestamp be used in the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, the timestamp is required by TON sign data spec

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.
impl MessageSigner {
fn get_ton_result(&self, result: &TonSignResult) -> Result<String, GemstoneError> {
let string = String::from_utf8(self.message.data.clone())?;
let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?;
let data = TonSignMessageData::from_bytes(string.as_bytes())?;

fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result<String, SignerError> {
let (signature, _public_key) = sign_personal(message, private_key)?;
Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature))
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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(|d| d.as_secs())?;

never unwrap for such cases.

mod tests {
use super::*;

const TEST_ADDRESS: &str = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg";
Copy link
Contributor

Choose a reason for hiding this comment

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

should be added to testkit.rs, see other examples

@DRadmir DRadmir merged commit 6fd70cb into main Mar 6, 2026
6 checks passed
@DRadmir DRadmir deleted the ton-walletconnect-fix branch March 6, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants