From 7145bb43ef370266afe3445f48c538a1923ea8e5 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 11 Mar 2026 17:15:27 -0600 Subject: [PATCH 1/3] feat(tron,ton): add TRON and TON chain support TRON: secp256k1 + Keccak256 address derivation, SHA256 tx signing TON: Ed25519 address derivation with CRC16 + Base64url, Ed25519 tx signing Both use existing trezor-crypto primitives only. --- deps/python-keepkey | 2 +- include/keepkey/transport/messages-ton.options | 2 ++ include/keepkey/transport/messages-tron.options | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index e89587701..f1dd2b684 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit e8958770152a85af4c2a03f9e93b5d98e8dff44b +Subproject commit f1dd2b6847346abe8ea2985b22d688de4911643f diff --git a/include/keepkey/transport/messages-ton.options b/include/keepkey/transport/messages-ton.options index 18c064289..bec6d9857 100644 --- a/include/keepkey/transport/messages-ton.options +++ b/include/keepkey/transport/messages-ton.options @@ -9,4 +9,6 @@ TonSignTx.coin_name max_size:21 TonSignTx.raw_tx max_size:1024 TonSignTx.to_address max_size:50 +TonAddress.address max_size:50 +TonAddress.raw_address max_size:70 TonSignedTx.signature max_size:64 diff --git a/include/keepkey/transport/messages-tron.options b/include/keepkey/transport/messages-tron.options index 80215e8cf..665310b95 100644 --- a/include/keepkey/transport/messages-tron.options +++ b/include/keepkey/transport/messages-tron.options @@ -11,4 +11,12 @@ TronSignTx.ref_block_hash max_size:32 TronSignTx.contract_type max_size:64 TronSignTx.to_address max_size:35 +TronSignTx.address_n max_count:8 +TronSignTx.coin_name max_size:21 +TronSignTx.raw_data max_size:1024 +TronSignTx.ref_block_bytes max_size:2 +TronSignTx.ref_block_hash max_size:8 +TronSignTx.contract_type max_size:50 +TronSignTx.to_address max_size:35 +TronAddress.address max_size:35 TronSignedTx.signature max_size:65 From e93d914a44a6b7f4ac21b0404ee5780c563f8a63 Mon Sep 17 00:00:00 2001 From: highlander Date: Tue, 17 Mar 2026 19:44:57 -0600 Subject: [PATCH 2/3] feat(tron): reconstruct-then-sign for clear-signed TRON transactions Security improvement: firmware reconstructs protobuf from structured fields (TronTransferContract, TronTriggerSmartContract) and signs the reconstruction. Display shows verified data, not host-asserted strings. - Structured mode: host sends transfer/trigger_smart fields, firmware rebuilds raw_data protobuf, displays verified amount/address, signs - Legacy mode: raw_data field still works with explicit blind-sign warning - TRC-20 ABI decoding: recognizes transfer(address,uint256) selector - 12 hardcoded TRC-20 tokens (USDT, USDC, SUN, BTT, etc.) - Bounded protobuf serialization with capacity checks - Address validation BEFORE display (prevents confusion attacks) - C unit tests for address, TRC-20 decoding, formatting - Device-protocol updated with structured message definitions Based on upstream keepkey/keepkey-firmware#387 (feat/tron-ton-support) --- deps/device-protocol | 2 +- include/keepkey/firmware/tron.h | 79 +++- .../keepkey/transport/messages-tron.options | 13 +- lib/firmware/fsm_msg_tron.h | 241 +++++++++-- lib/firmware/tron.c | 390 ++++++++++++++++-- lib/firmware/tron_tokens.h | 45 ++ unittests/firmware/CMakeLists.txt | 1 + unittests/firmware/tron.cpp | 128 ++++++ 8 files changed, 822 insertions(+), 77 deletions(-) create mode 100644 lib/firmware/tron_tokens.h create mode 100644 unittests/firmware/tron.cpp diff --git a/deps/device-protocol b/deps/device-protocol index b07589ff3..acd10830f 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit b07589ff386265f894736a490fc163d46f9479b7 +Subproject commit acd10830f606751105b75b34c0bb60b15935ec8a diff --git a/include/keepkey/firmware/tron.h b/include/keepkey/firmware/tron.h index 22abb5c26..cb6b79f1a 100644 --- a/include/keepkey/firmware/tron.h +++ b/include/keepkey/firmware/tron.h @@ -27,32 +27,89 @@ // TRON address length (Base58Check, typically 34 chars starting with 'T') #define TRON_ADDRESS_MAX_LEN 64 +// TRON address raw size (1 prefix + 20 hash) +#define TRON_ADDRESS_SIZE 21 + +// TRON max encoded address size +#define MAX_TRON_ADDR_SIZE 35 + +// TRON mainnet address prefix +#define TRON_MAINNET_PREFIX 0x41 + // TRON decimals (1 TRX = 1,000,000 SUN) #define TRON_DECIMALS 6 +// TRON signature size (r + s + recovery_id) +#define TRON_SIGNATURE_SIZE 65 + +// TRC-20 transfer(address,uint256) function selector +#define TRC20_TRANSFER_SELECTOR 0xa9059cbb + /** * Generate TRON address from secp256k1 public key - * @param public_key secp256k1 public key (33 bytes compressed) - * @param address Output buffer for Base58Check encoded address - * @param address_len Length of output buffer - * @return true on success, false on failure */ bool tron_getAddress(const uint8_t public_key[33], char *address, size_t address_len); +/** + * Decode a Base58Check TRON address to raw 21-byte form + * @param address Base58Check encoded "T..." address + * @param raw_address Output buffer (21 bytes: 0x41 prefix + 20 hash) + * @return true on success, false if decode fails or prefix is wrong + */ +bool tron_decodeAddress(const char *address, + uint8_t raw_address[TRON_ADDRESS_SIZE]); + +/** + * Validate a Base58Check TRON address + */ +bool tron_validateAddress(const char *address); + /** * Format TRON amount (SUN) for display - * @param buf Output buffer - * @param len Length of output buffer - * @param amount Amount in SUN (1 TRX = 1,000,000 SUN) */ void tron_formatAmount(char *buf, size_t len, uint64_t amount); /** - * Sign a TRON transaction - * @param node HD node containing private key - * @param msg TronSignTx request message - * @param resp TronSignedTx response message (will be filled with signature) + * Format a token amount with given decimals for display + * @param buf Output buffer + * @param buf_len Buffer size + * @param amount_be 32-byte big-endian amount + * @param decimals Token decimal places + * @param symbol Token symbol (e.g., "USDT") + */ +void tron_formatTokenAmount(char *buf, size_t buf_len, + const uint8_t amount_be[32], + uint8_t decimals, const char *symbol); + +/** + * Decode TRC-20 transfer(address,uint256) ABI call data + * @param data ABI-encoded call data (>= 68 bytes) + * @param data_len Length of call data + * @param to_raw Output: 21-byte TRON address (0x41 + 20-byte EVM addr) + * @param amount_bytes Output: 32-byte big-endian amount + * @return true if data matches transfer(address,uint256) selector + */ +bool tron_decodeTRC20Transfer(const uint8_t *data, size_t data_len, + uint8_t to_raw[TRON_ADDRESS_SIZE], + uint8_t amount_bytes[32]); + +/** + * Reconstruct Transaction.raw protobuf from structured fields + * @param msg TronSignTx with structured fields + * @param owner_raw 21-byte raw address of the signer + * @param out Output buffer for serialized protobuf + * @param out_len In: max capacity; Out: bytes written + * @param max_len Maximum buffer size + * @return true on success + */ +bool tron_serializeRawTransaction(const TronSignTx *msg, + const uint8_t *owner_raw, + uint8_t *out, size_t *out_len, + size_t max_len); + +/** + * Sign a TRON transaction (supports both structured and legacy modes) */ bool tron_signTx(const HDNode *node, const TronSignTx *msg, TronSignedTx *resp); diff --git a/include/keepkey/transport/messages-tron.options b/include/keepkey/transport/messages-tron.options index 665310b95..05aef1433 100644 --- a/include/keepkey/transport/messages-tron.options +++ b/include/keepkey/transport/messages-tron.options @@ -3,13 +3,19 @@ TronGetAddress.coin_name max_size:21 TronAddress.address max_size:35 +TronTransferContract.to_address max_size:35 + +TronTriggerSmartContract.contract_address max_size:35 +TronTriggerSmartContract.data max_size:512 + TronSignTx.address_n max_count:8 TronSignTx.coin_name max_size:21 -TronSignTx.raw_data max_size:2048 -TronSignTx.ref_block_bytes max_size:4 -TronSignTx.ref_block_hash max_size:32 +TronSignTx.raw_data max_size:1024 +TronSignTx.ref_block_bytes max_size:2 +TronSignTx.ref_block_hash max_size:8 TronSignTx.contract_type max_size:64 TronSignTx.to_address max_size:35 +TronSignTx.data max_size:256 TronSignTx.address_n max_count:8 TronSignTx.coin_name max_size:21 @@ -20,3 +26,4 @@ TronSignTx.contract_type max_size:50 TronSignTx.to_address max_size:35 TronAddress.address max_size:35 TronSignedTx.signature max_size:65 +TronSignedTx.serialized_tx max_size:1024 diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index beea2f769..72de585c6 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -17,6 +17,8 @@ * along with this library. If not, see . */ +#include "tron_tokens.h" + void fsm_msgTronGetAddress(const TronGetAddress *msg) { RESP_INIT(TronAddress); @@ -34,13 +36,11 @@ void fsm_msgTronGetAddress(const TronGetAddress *msg) { return; } - // Derive node using secp256k1 curve HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, msg->address_n_count, NULL); if (!node) return; hdnode_fill_public_key(node); - // Get TRON address from public key (Base58Check with prefix 'T') char address[MAX_ADDR_SIZE]; if (!tron_getAddress(node->public_key, address, sizeof(address))) { memzero(node, sizeof(*node)); @@ -52,7 +52,6 @@ void fsm_msgTronGetAddress(const TronGetAddress *msg) { resp->has_address = true; strlcpy(resp->address, address, sizeof(resp->address)); - // Show address on display if requested if (msg->has_show_display && msg->show_display) { char node_str[NODE_STRING_LENGTH]; if (!bip32_path_to_string(node_str, sizeof(node_str), msg->address_n, @@ -91,47 +90,241 @@ void fsm_msgTronSignTx(TronSignTx *msg) { return; } - - // Derive node using secp256k1 curve HDNode *node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, msg->address_n_count, NULL); if (!node) return; hdnode_fill_public_key(node); - if (!msg->has_raw_data || msg->raw_data.size == 0) { + bool is_structured = msg->has_transfer || msg->has_trigger_smart; + bool is_legacy = msg->has_raw_data && msg->raw_data.size > 0; + + if (!is_structured && !is_legacy) { memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_Other, - _("Missing transaction data")); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Must provide transfer, trigger_smart, or raw_data")); layoutHome(); return; } - bool needs_confirm = true; + /* ---- Structured reconstruct-then-sign path ---- */ + if (is_structured) { + /* Validate required header fields */ + if (!msg->has_ref_block_bytes || msg->ref_block_bytes.size != 2) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("ref_block_bytes must be exactly 2 bytes")); + layoutHome(); + return; + } + if (!msg->has_ref_block_hash || msg->ref_block_hash.size != 8) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("ref_block_hash must be exactly 8 bytes")); + layoutHome(); + return; + } + if (!msg->has_expiration) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("expiration is required")); + layoutHome(); + return; + } - // Display transaction details if available - if (needs_confirm && msg->has_to_address && msg->has_amount) { - char amount_str[32]; - tron_formatAmount(amount_str, sizeof(amount_str), msg->amount); + /* Show memo if present */ + if (msg->has_data && msg->data.size > 0) { + char memo_hex[65]; + size_t show = msg->data.size < 32 ? msg->data.size : 32; + for (size_t i = 0; i < show; i++) { + snprintf(memo_hex + i * 2, 3, "%02x", msg->data.bytes[i]); + } + if (msg->data.size > 32) { + strlcat(memo_hex, "...", sizeof(memo_hex)); + } + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Memo", "Transaction memo:\n%s", memo_hex)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + + /* ---- TransferContract path ---- */ + if (msg->has_transfer) { + /* Validate to_address BEFORE display */ + if (!tron_validateAddress(msg->transfer.to_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid to_address in transfer")); + layoutHome(); + return; + } + + char amount_str[64]; + tron_formatAmount(amount_str, sizeof(amount_str), msg->transfer.amount); + + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Send TRX", "Send %s to\n%s?", + amount_str, msg->transfer.to_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } - if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, - "Send", "Send %s TRX to %s?", - amount_str, msg->to_address)) { + /* ---- TriggerSmartContract path ---- */ + if (msg->has_trigger_smart) { + /* Validate contract_address BEFORE any display */ + if (!tron_validateAddress(msg->trigger_smart.contract_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid contract_address")); + layoutHome(); + return; + } + + /* Attempt TRC-20 transfer(address,uint256) decode */ + uint8_t trc20_to[TRON_ADDRESS_SIZE]; + uint8_t trc20_amount[32]; + bool is_trc20 = msg->trigger_smart.has_data && + msg->trigger_smart.data.size >= 68 && + tron_decodeTRC20Transfer(msg->trigger_smart.data.bytes, + msg->trigger_smart.data.size, + trc20_to, trc20_amount); + + if (is_trc20) { + /* Look up known token by contract address */ + const TronToken *token = + tron_token_by_address(msg->trigger_smart.contract_address); + + char to_addr[MAX_TRON_ADDR_SIZE]; + if (!base58_encode_check(trc20_to, 21, HASHER_SHA2D, to_addr, + sizeof(to_addr))) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("Failed to encode recipient address")); + layoutHome(); + return; + } + + if (token) { + char amt_str[64]; + tron_formatTokenAmount(amt_str, sizeof(amt_str), trc20_amount, + token->decimals, token->symbol); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Send Token", "Send %s to\n%s?", + amt_str, to_addr)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } else { + /* Unknown TRC-20 token */ + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Unknown Token", + "Transfer unknown token at\n%s\nto %s?", + msg->trigger_smart.contract_address, to_addr)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + } else { + /* Not a TRC-20 transfer — generic smart contract call */ + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Contract Call", + "Call contract\n%s?\nCannot verify call data.", + msg->trigger_smart.contract_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + + /* Show call_value if sending TRX along with the smart contract call */ + if (msg->trigger_smart.has_call_value && + msg->trigger_smart.call_value > 0) { + char call_str[64]; + tron_formatAmount(call_str, sizeof(call_str), + msg->trigger_smart.call_value); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Call Value", + "Also sending %s with call?", call_str)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + } + + /* Show fee limit if present */ + if (msg->has_fee_limit && msg->fee_limit > 0) { + char fee_str[64]; + tron_formatAmount(fee_str, sizeof(fee_str), msg->fee_limit); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Fee Limit", "Maximum fee:\n%s?", fee_str)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + + /* Final confirmation */ + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Sign", + "Sign this TRON transaction?")) { memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); layoutHome(); return; } } - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TRON transaction?")) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); - layoutHome(); - return; + /* ---- Legacy blind-sign path ---- */ + if (!is_structured) { + /* Blind-sign warning: user must understand this is unverified */ + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, + "Blind Sign", + "Sign unverified TRON\ntransaction?\n" + "Data cannot be verified\non-device.")) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + + /* Show optional unverified hints from host */ + if (msg->has_to_address && msg->has_amount) { + char amount_str[64]; + tron_formatAmount(amount_str, sizeof(amount_str), msg->amount); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Unverified", "Send %s to\n%s?\n(UNVERIFIED)", + amount_str, msg->to_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } } - // Sign the transaction with secp256k1 + /* Sign the transaction */ if (!tron_signTx(node, msg, resp)) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_Other, _("TRON signing failed")); diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index cbd5b0b4b..5a80973e1 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -24,17 +24,66 @@ #include "trezor/crypto/ecdsa.h" #include "trezor/crypto/memzero.h" #include "trezor/crypto/secp256k1.h" +#include "trezor/crypto/sha2.h" #include "trezor/crypto/sha3.h" +#include #include -#define TRON_ADDRESS_PREFIX 0x41 // Mainnet addresses start with 'T' +/* ------------------------------------------------------------------ */ +/* Bounded protobuf encoding helpers */ +/* ------------------------------------------------------------------ */ + +/* How many bytes a varint takes. */ +static size_t pb_varint_size(uint64_t v) { + size_t n = 1; + while (v >= 0x80) { v >>= 7; n++; } + return n; +} + +/* Encode a varint, checking remaining capacity. */ +static bool pb_encode_varint_safe(uint8_t *buf, size_t *pos, size_t cap, + uint64_t v) { + size_t need = pb_varint_size(v); + if (*pos + need > cap) return false; + while (v >= 0x80) { + buf[(*pos)++] = (uint8_t)(v & 0x7F) | 0x80; + v >>= 7; + } + buf[(*pos)++] = (uint8_t)v; + return true; +} + +/* Write a protobuf tag (field_num << 3 | wire_type). */ +static bool pb_write_tag_safe(uint8_t *buf, size_t *pos, size_t cap, + unsigned field, unsigned wire) { + return pb_encode_varint_safe(buf, pos, cap, + ((uint64_t)field << 3) | wire); +} + +/* Write a length-delimited field (wire type 2). */ +static bool pb_write_bytes_safe(uint8_t *buf, size_t *pos, size_t cap, + unsigned field, + const uint8_t *data, size_t data_len) { + if (!pb_write_tag_safe(buf, pos, cap, field, 2)) return false; + if (!pb_encode_varint_safe(buf, pos, cap, data_len)) return false; + if (*pos + data_len > cap) return false; + memcpy(buf + *pos, data, data_len); + *pos += data_len; + return true; +} + +/* Write a varint field (wire type 0). */ +static bool pb_write_varint_field_safe(uint8_t *buf, size_t *pos, size_t cap, + unsigned field, uint64_t val) { + if (!pb_write_tag_safe(buf, pos, cap, field, 0)) return false; + return pb_encode_varint_safe(buf, pos, cap, val); +} + +/* ------------------------------------------------------------------ */ +/* Address functions */ +/* ------------------------------------------------------------------ */ -/** - * Generate TRON address from secp256k1 public key - * TRON uses Keccak256(uncompressed_pubkey) and takes last 20 bytes, - * then prepends 0x41 and Base58Check encodes it - */ bool tron_getAddress(const uint8_t public_key[33], char *address, size_t address_len) { if (address_len < TRON_ADDRESS_MAX_LEN) { @@ -45,88 +94,353 @@ bool tron_getAddress(const uint8_t public_key[33], char *address, uint8_t hash[32]; uint8_t addr_bytes[21]; - // Uncompress the public key if (!ecdsa_uncompress_pubkey(&secp256k1, public_key, uncompressed_pubkey)) { return false; } - // Keccak256 hash of uncompressed public key (skip first 0x04 byte) keccak_256(uncompressed_pubkey + 1, 64, hash); - // Take last 20 bytes of hash and prepend TRON prefix byte - addr_bytes[0] = TRON_ADDRESS_PREFIX; + addr_bytes[0] = TRON_MAINNET_PREFIX; memcpy(addr_bytes + 1, hash + 12, 20); - // Base58Check encode with double SHA256 if (!base58_encode_check(addr_bytes, 21, HASHER_SHA2D, address, address_len)) { + memzero(uncompressed_pubkey, sizeof(uncompressed_pubkey)); + memzero(hash, sizeof(hash)); return false; } - // Clean up sensitive data memzero(uncompressed_pubkey, sizeof(uncompressed_pubkey)); memzero(hash, sizeof(hash)); + return true; +} +bool tron_decodeAddress(const char *address, + uint8_t raw_address[TRON_ADDRESS_SIZE]) { + if (!address || strlen(address) < 25 || strlen(address) > 36) { + return false; + } + uint8_t decoded[25]; /* 21-byte payload + 4-byte checksum */ + int len = base58_decode_check(address, HASHER_SHA2D, decoded, + sizeof(decoded)); + if (len != 21) return false; + if (decoded[0] != TRON_MAINNET_PREFIX) return false; + memcpy(raw_address, decoded, 21); return true; } -/** - * Format TRON amount (SUN) for display - * 1 TRX = 1,000,000 SUN - */ +bool tron_validateAddress(const char *address) { + uint8_t raw[TRON_ADDRESS_SIZE]; + return tron_decodeAddress(address, raw); +} + +/* ------------------------------------------------------------------ */ +/* Formatting */ +/* ------------------------------------------------------------------ */ + void tron_formatAmount(char *buf, size_t len, uint64_t amount) { bignum256 val; bn_read_uint64(amount, &val); bn_format(&val, NULL, " TRX", TRON_DECIMALS, 0, false, buf, len); } -/** - * Sign a TRON transaction with secp256k1 - */ -bool tron_signTx(const HDNode *node, const TronSignTx *msg, - TronSignedTx *resp) { - if (!node || !msg || !resp) { +void tron_formatTokenAmount(char *buf, size_t buf_len, + const uint8_t amount_be[32], + uint8_t decimals, const char *symbol) { + /* Check if amount fits in uint64 (first 24 bytes must be zero). */ + bool fits_u64 = true; + for (int i = 0; i < 24; i++) { + if (amount_be[i] != 0) { fits_u64 = false; break; } + } + + if (fits_u64) { + uint64_t val64 = 0; + for (int i = 24; i < 32; i++) { + val64 = (val64 << 8) | amount_be[i]; + } + bignum256 bn; + bn_read_uint64(val64, &bn); + char suffix[20]; + snprintf(suffix, sizeof(suffix), " %s", symbol); + bn_format(&bn, NULL, suffix, decimals, 0, false, buf, buf_len); + } else { + snprintf(buf, buf_len, "(raw) %s", symbol); + } +} + +/* ------------------------------------------------------------------ */ +/* TRC-20 ABI decoding */ +/* ------------------------------------------------------------------ */ + +bool tron_decodeTRC20Transfer(const uint8_t *data, size_t data_len, + uint8_t to_raw[TRON_ADDRESS_SIZE], + uint8_t amount_bytes[32]) { + if (data_len < 68) return false; + + /* Check 4-byte function selector: transfer(address,uint256) = 0xa9059cbb */ + if (data[0] != 0xa9 || data[1] != 0x05 || + data[2] != 0x9c || data[3] != 0xbb) { return false; } - // Verify we have raw transaction data - if (!msg->has_raw_data || msg->raw_data.size == 0) { + /* Bytes 4..35: ABI-encoded address (12 zero bytes + 20-byte EVM address). + * TRON ABI uses standard Solidity encoding — no 0x41 prefix in the ABI. + * We verify the 12 leading bytes are zero, then prepend 0x41 ourselves. */ + for (int i = 4; i < 16; i++) { + if (data[i] != 0) return false; + } + to_raw[0] = TRON_MAINNET_PREFIX; + memcpy(to_raw + 1, data + 16, 20); + + /* Bytes 36..67: uint256 amount (big-endian) */ + memcpy(amount_bytes, data + 36, 32); + return true; +} + +/* ------------------------------------------------------------------ */ +/* Protobuf serialization (reconstruct-then-sign) */ +/* ------------------------------------------------------------------ */ + +/* Serialize a TransferContract into a contract entry (field 11 of raw). */ +static bool tron_serializeTransferContract(uint8_t *buf, size_t *pos, + size_t cap, + const TronTransferContract *tc, + const uint8_t *owner_raw) { + /* Build inner TransferContract protobuf: + * field 1 (bytes): owner_address (21 bytes) + * field 2 (bytes): to_address (21 bytes) + * field 3 (varint): amount + */ + uint8_t inner[128]; + size_t ip = 0; + + /* owner_address */ + if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 1, owner_raw, 21)) + return false; + + /* to_address — decode from Base58 */ + uint8_t to_raw[TRON_ADDRESS_SIZE]; + if (!tron_decodeAddress(tc->to_address, to_raw)) return false; + if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 2, to_raw, 21)) + return false; + + /* amount */ + if (!pb_write_varint_field_safe(inner, &ip, sizeof(inner), 3, tc->amount)) + return false; + + /* Build contract wrapper: + * field 1 (varint): type = 1 (TransferContract) + * field 2 (bytes): google.protobuf.Any { + * field 1 (bytes): type_url + * field 2 (bytes): value = inner + * } + */ + static const char type_url[] = + "type.googleapis.com/protocol.TransferContract"; + uint8_t any[256]; + size_t ap = 0; + if (!pb_write_bytes_safe(any, &ap, sizeof(any), 1, + (const uint8_t *)type_url, strlen(type_url))) + return false; + if (!pb_write_bytes_safe(any, &ap, sizeof(any), 2, inner, ip)) + return false; + + uint8_t contract[300]; + size_t cp = 0; + if (!pb_write_varint_field_safe(contract, &cp, sizeof(contract), 1, 1)) + return false; + if (!pb_write_bytes_safe(contract, &cp, sizeof(contract), 2, any, ap)) + return false; + + /* Write as field 11 of the outer transaction.raw */ + return pb_write_bytes_safe(buf, pos, cap, 11, contract, cp); +} + +/* Serialize a TriggerSmartContract into a contract entry. */ +static bool tron_serializeTriggerSmartContract( + uint8_t *buf, size_t *pos, size_t cap, + const TronTriggerSmartContract *tsc, const uint8_t *owner_raw) { + /* Inner TriggerSmartContract: + * field 1 (bytes): owner_address (21) + * field 2 (bytes): contract_address (21) + * field 3 (bytes): data (ABI call data) + * field 4 (varint): call_value + */ + uint8_t inner[700]; + size_t ip = 0; + + if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 1, owner_raw, 21)) + return false; + + uint8_t contract_raw[TRON_ADDRESS_SIZE]; + if (!tron_decodeAddress(tsc->contract_address, contract_raw)) return false; + if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 2, contract_raw, 21)) return false; + + if (tsc->has_data && tsc->data.size > 0) { + if (!pb_write_bytes_safe(inner, &ip, sizeof(inner), 3, + tsc->data.bytes, tsc->data.size)) + return false; } - // Get the curve for secp256k1 - const curve_info *curve = get_curve_by_name(SECP256K1_NAME); - if (!curve) { + if (tsc->has_call_value && tsc->call_value > 0) { + if (!pb_write_varint_field_safe(inner, &ip, sizeof(inner), 4, + tsc->call_value)) + return false; + } + + /* Wrap in Any with type_url for TriggerSmartContract */ + static const char type_url[] = + "type.googleapis.com/protocol.TriggerSmartContract"; + uint8_t any[768]; + size_t ap = 0; + if (!pb_write_bytes_safe(any, &ap, sizeof(any), 1, + (const uint8_t *)type_url, strlen(type_url))) + return false; + if (!pb_write_bytes_safe(any, &ap, sizeof(any), 2, inner, ip)) + return false; + + /* contract type = 31 for TriggerSmartContract */ + uint8_t contract[900]; + size_t cpp = 0; + if (!pb_write_varint_field_safe(contract, &cpp, sizeof(contract), 1, 31)) + return false; + if (!pb_write_bytes_safe(contract, &cpp, sizeof(contract), 2, any, ap)) + return false; + + return pb_write_bytes_safe(buf, pos, cap, 11, contract, cpp); +} + +bool tron_serializeRawTransaction(const TronSignTx *msg, + const uint8_t *owner_raw, + uint8_t *out, size_t *out_len, + size_t max_len) { + size_t pos = 0; + + /* Field 1: ref_block_bytes (2 bytes required) */ + if (!msg->has_ref_block_bytes || msg->ref_block_bytes.size != 2) + return false; + if (!pb_write_bytes_safe(out, &pos, max_len, 1, + msg->ref_block_bytes.bytes, 2)) + return false; + + /* Field 4: ref_block_hash (8 bytes required) */ + if (!msg->has_ref_block_hash || msg->ref_block_hash.size != 8) + return false; + if (!pb_write_bytes_safe(out, &pos, max_len, 4, + msg->ref_block_hash.bytes, 8)) + return false; + + /* Field 8: expiration */ + if (!msg->has_expiration) return false; + if (!pb_write_varint_field_safe(out, &pos, max_len, 8, msg->expiration)) return false; + + /* Field 10: data/memo (optional, max 256 bytes) */ + if (msg->has_data && msg->data.size > 0) { + if (msg->data.size > 256) return false; + if (!pb_write_bytes_safe(out, &pos, max_len, 10, + msg->data.bytes, msg->data.size)) + return false; + } + + /* Field 11: contract (exactly one) */ + if (msg->has_transfer) { + if (!tron_serializeTransferContract(out, &pos, max_len, + &msg->transfer, owner_raw)) + return false; + } else if (msg->has_trigger_smart) { + if (!tron_serializeTriggerSmartContract(out, &pos, max_len, + &msg->trigger_smart, owner_raw)) + return false; + } else { + return false; /* Must have exactly one contract */ + } + + /* Field 14: timestamp (optional) */ + if (msg->has_timestamp) { + if (!pb_write_varint_field_safe(out, &pos, max_len, 14, msg->timestamp)) + return false; + } + + /* Field 18: fee_limit (optional) */ + if (msg->has_fee_limit && msg->fee_limit > 0) { + if (!pb_write_varint_field_safe(out, &pos, max_len, 18, msg->fee_limit)) + return false; } - // Hash the transaction with SHA256 + *out_len = pos; + return true; +} + +/* ------------------------------------------------------------------ */ +/* Transaction signing */ +/* ------------------------------------------------------------------ */ + +bool tron_signTx(const HDNode *node, const TronSignTx *msg, + TronSignedTx *resp) { + if (!node || !msg || !resp) return false; + + const curve_info *curve = get_curve_by_name(SECP256K1_NAME); + if (!curve) return false; + uint8_t hash[32]; - sha256_Raw(msg->raw_data.bytes, msg->raw_data.size, hash); + bool is_structured = msg->has_transfer || msg->has_trigger_smart; + + if (is_structured) { + /* Derive owner address from public key */ + uint8_t owner_raw[TRON_ADDRESS_SIZE]; + { + uint8_t uncompressed[65]; + uint8_t keccak[32]; + if (!ecdsa_uncompress_pubkey(&secp256k1, node->public_key, uncompressed)) + return false; + keccak_256(uncompressed + 1, 64, keccak); + owner_raw[0] = TRON_MAINNET_PREFIX; + memcpy(owner_raw + 1, keccak + 12, 20); + memzero(uncompressed, sizeof(uncompressed)); + memzero(keccak, sizeof(keccak)); + } + + /* Reconstruct the raw transaction */ + uint8_t raw_buf[1024]; + size_t raw_len = sizeof(raw_buf); + if (!tron_serializeRawTransaction(msg, owner_raw, raw_buf, &raw_len, + sizeof(raw_buf))) { + memzero(owner_raw, sizeof(owner_raw)); + return false; + } + memzero(owner_raw, sizeof(owner_raw)); + + /* Return the serialized bytes for host verification */ + resp->has_serialized_tx = true; + resp->serialized_tx.size = raw_len; + memcpy(resp->serialized_tx.bytes, raw_buf, raw_len); - // Sign with secp256k1 (recoverable signature: 65 bytes including recovery - // ID) + sha256_Raw(raw_buf, raw_len, hash); + memzero(raw_buf, sizeof(raw_buf)); + } else if (msg->has_raw_data && msg->raw_data.size > 0) { + /* Legacy: hash the client-provided raw_data */ + sha256_Raw(msg->raw_data.bytes, msg->raw_data.size, hash); + } else { + return false; + } + + /* Sign with secp256k1 */ uint8_t sig[65]; uint8_t pby; - if (ecdsa_sign_digest(&secp256k1, node->private_key, hash, sig, &pby, NULL) != 0) { memzero(hash, sizeof(hash)); return false; } - - // Convert to recoverable signature format (r + s + recovery_id) - // The recovery ID allows recovering the public key from the signature sig[64] = pby; - // Copy signature to response (65 bytes) resp->has_signature = true; resp->signature.size = 65; memcpy(resp->signature.bytes, sig, 65); - // Clean up sensitive data memzero(hash, sizeof(hash)); memzero(sig, sizeof(sig)); - return true; } diff --git a/lib/firmware/tron_tokens.h b/lib/firmware/tron_tokens.h new file mode 100644 index 000000000..384bcfbc5 --- /dev/null +++ b/lib/firmware/tron_tokens.h @@ -0,0 +1,45 @@ +/* + * Hardcoded TRC-20 token definitions for on-device display. + * Only high-volume tokens are included; unknown tokens show as raw amounts. + */ + +#ifndef KEEPKEY_FIRMWARE_TRON_TOKENS_H +#define KEEPKEY_FIRMWARE_TRON_TOKENS_H + +#include +#include + +typedef struct { + const char *address; // Base58Check contract address + const char *symbol; // Token ticker + uint8_t decimals; // Token decimal places +} TronToken; + +static const TronToken tron_tokens[] = { + {"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "USDT", 6}, + {"TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn", "USDD", 18}, + {"TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S", "SUN", 18}, + {"TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9", "JST", 18}, + {"TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4", "BTT", 18}, + {"TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7", "WIN", 6}, + {"TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65", "WBTC", 8}, + {"THb4CqiFdwNHsWsQCs4JhzwjMWys4aqCbF", "ETH", 18}, + {"TDPJwSHLPDqvDeX1JLkP2z1szHEhLLVBFq", "USD1", 6}, + {"TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6", "HTX", 18}, + {"TUpMhErZL2fhh4sVNULAbNKLokS4GjC1F4", "TUSD", 18}, + {"TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", "USDC", 6}, +}; + +#define TRON_TOKEN_COUNT (sizeof(tron_tokens) / sizeof(tron_tokens[0])) + +static inline const TronToken *tron_token_by_address( + const char *contract_address) { + for (size_t i = 0; i < TRON_TOKEN_COUNT; i++) { + if (strcmp(tron_tokens[i].address, contract_address) == 0) { + return &tron_tokens[i]; + } + } + return NULL; +} + +#endif diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt index 221580e16..d07b07481 100644 --- a/unittests/firmware/CMakeLists.txt +++ b/unittests/firmware/CMakeLists.txt @@ -3,6 +3,7 @@ set(sources recovery.cpp storage.cpp ton.cpp + tron.cpp usb_rx.cpp u2f.cpp) diff --git a/unittests/firmware/tron.cpp b/unittests/firmware/tron.cpp new file mode 100644 index 000000000..32a70ead6 --- /dev/null +++ b/unittests/firmware/tron.cpp @@ -0,0 +1,128 @@ +extern "C" { +#include "keepkey/firmware/tron.h" +#include "messages-tron.pb.h" +#include +} + +#include "gtest/gtest.h" + +/* ------------------------------------------------------------------ */ +/* Address encoding / decoding tests */ +/* ------------------------------------------------------------------ */ + +TEST(Tron, DecodeValidAddress) { + // TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (USDT contract) + uint8_t raw[TRON_ADDRESS_SIZE]; + ASSERT_TRUE(tron_decodeAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", raw)); + EXPECT_EQ(raw[0], 0x41); +} + +TEST(Tron, DecodeInvalidAddress) { + uint8_t raw[TRON_ADDRESS_SIZE]; + // Too short + EXPECT_FALSE(tron_decodeAddress("T", raw)); + // Bad checksum + EXPECT_FALSE(tron_decodeAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6X", raw)); + // NULL + EXPECT_FALSE(tron_decodeAddress(NULL, raw)); +} + +TEST(Tron, ValidateAddress) { + EXPECT_TRUE(tron_validateAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")); + EXPECT_FALSE(tron_validateAddress("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6X")); + EXPECT_FALSE(tron_validateAddress(NULL)); +} + +/* ------------------------------------------------------------------ */ +/* TRC-20 ABI decoding tests */ +/* ------------------------------------------------------------------ */ + +TEST(Tron, DecodeTRC20Transfer) { + // Real TRC-20 transfer(address,uint256) ABI encoding: + // selector: a9059cbb + // to: 000000000000000000000000 + 20-byte EVM address + // amount: 32-byte big-endian + uint8_t data[68]; + memset(data, 0, sizeof(data)); + + // selector + data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb; + // EVM address at bytes 16..35 (12 zero pad + 20 addr) + data[16] = 0xDE; data[17] = 0xAD; data[35] = 0xBE; + // Amount: 1000000 (0xF4240) at last 3 bytes + data[65] = 0x0F; data[66] = 0x42; data[67] = 0x40; + + uint8_t to_raw[TRON_ADDRESS_SIZE]; + uint8_t amount[32]; + ASSERT_TRUE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount)); + + // Must prepend 0x41 TRON prefix + EXPECT_EQ(to_raw[0], 0x41); + EXPECT_EQ(to_raw[1], 0xDE); + EXPECT_EQ(to_raw[2], 0xAD); + + // Amount check + EXPECT_EQ(amount[29], 0x0F); + EXPECT_EQ(amount[30], 0x42); + EXPECT_EQ(amount[31], 0x40); +} + +TEST(Tron, DecodeTRC20WrongSelector) { + uint8_t data[68]; + memset(data, 0, sizeof(data)); + data[0] = 0x12; data[1] = 0x34; data[2] = 0x56; data[3] = 0x78; + + uint8_t to_raw[TRON_ADDRESS_SIZE]; + uint8_t amount[32]; + EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount)); +} + +TEST(Tron, DecodeTRC20TooShort) { + uint8_t data[60]; + memset(data, 0, sizeof(data)); + data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb; + + uint8_t to_raw[TRON_ADDRESS_SIZE]; + uint8_t amount[32]; + EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount)); +} + +TEST(Tron, DecodeTRC20NonZeroPadding) { + // If the 12 leading pad bytes aren't all zero, reject + uint8_t data[68]; + memset(data, 0, sizeof(data)); + data[0] = 0xa9; data[1] = 0x05; data[2] = 0x9c; data[3] = 0xbb; + data[4] = 0x01; // non-zero in padding area + + uint8_t to_raw[TRON_ADDRESS_SIZE]; + uint8_t amount[32]; + EXPECT_FALSE(tron_decodeTRC20Transfer(data, sizeof(data), to_raw, amount)); +} + +/* ------------------------------------------------------------------ */ +/* Formatting tests */ +/* ------------------------------------------------------------------ */ + +TEST(Tron, FormatAmount) { + char buf[64]; + + tron_formatAmount(buf, sizeof(buf), 1000000); // 1 TRX + EXPECT_STREQ(buf, "1 TRX"); + + tron_formatAmount(buf, sizeof(buf), 500000); // 0.5 TRX + EXPECT_STREQ(buf, "0.5 TRX"); + + tron_formatAmount(buf, sizeof(buf), 0); + EXPECT_STREQ(buf, "0 TRX"); +} + +TEST(Tron, FormatTokenAmount) { + char buf[64]; + + // 1 USDT (6 decimals) = 1000000 + uint8_t amount1[32]; + memset(amount1, 0, 32); + amount1[29] = 0x0F; amount1[30] = 0x42; amount1[31] = 0x40; // 1000000 + tron_formatTokenAmount(buf, sizeof(buf), amount1, 6, "USDT"); + EXPECT_STREQ(buf, "1 USDT"); +} From 314e6e3c6ad4204780805ae9c6831dd3badecdce Mon Sep 17 00:00:00 2001 From: highlander Date: Tue, 17 Mar 2026 20:33:50 -0600 Subject: [PATCH 3/3] fix(tron): reject when both transfer and trigger_smart are set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proto defines transfer and trigger_smart as separate optional fields, not a oneof. The serializer only processes one (if/else if), so if both are present the UI would walk both confirmation branches but only one gets signed — a display/signature mismatch. Reject with SyntaxError before any confirmation dialog. --- lib/firmware/fsm_msg_tron.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 72de585c6..48d2b6452 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -106,6 +106,15 @@ void fsm_msgTronSignTx(TronSignTx *msg) { return; } + /* Reject if both contract types are present — exactly one required */ + if (msg->has_transfer && msg->has_trigger_smart) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Cannot set both transfer and trigger_smart")); + layoutHome(); + return; + } + /* ---- Structured reconstruct-then-sign path ---- */ if (is_structured) { /* Validate required header fields */