diff --git a/include/keepkey/firmware/solana_tx.h b/include/keepkey/firmware/solana_tx.h index a5bbe16a8..319f284b1 100644 --- a/include/keepkey/firmware/solana_tx.h +++ b/include/keepkey/firmware/solana_tx.h @@ -60,7 +60,8 @@ typedef struct { uint16_t num_accounts; uint8_t account_keys[16][32]; // 16 accounts (512 bytes) uint8_t recent_blockhash[32]; - uint8_t num_instructions; + uint8_t num_instructions; // min(actual, 8) — parsed instructions + uint16_t total_instructions; // actual count from TX (for display) SolanaInstruction instructions[8]; // 8 instructions } SolanaParsedTransaction; diff --git a/include/keepkey/firmware/tron.h b/include/keepkey/firmware/tron.h index 22abb5c26..32b97ba02 100644 --- a/include/keepkey/firmware/tron.h +++ b/include/keepkey/firmware/tron.h @@ -57,4 +57,22 @@ void tron_formatAmount(char *buf, size_t len, uint64_t amount); bool tron_signTx(const HDNode *node, const TronSignTx *msg, TronSignedTx *resp); +/** + * Parsed TRON TransferContract fields extracted from raw_data. + * Used to verify on-device display against the actual signed payload. + */ +typedef struct { + uint8_t to_address[21]; // 0x41 prefix + 20 bytes + int64_t amount; // SUN + bool valid; // true if parsing succeeded +} TronParsedTransfer; + +/** + * Parse a TRON Transaction.raw protobuf to extract TransferContract fields. + * Only succeeds for simple TRX transfers (contract type 1). + * @return true if a TransferContract was found and parsed + */ +bool tron_parseTransfer(const uint8_t *raw_data, size_t raw_data_len, + TronParsedTransfer *out); + #endif diff --git a/lib/firmware/fsm_msg_solana.h b/lib/firmware/fsm_msg_solana.h index f163da372..6a59745f9 100644 --- a/lib/firmware/fsm_msg_solana.h +++ b/lib/firmware/fsm_msg_solana.h @@ -192,16 +192,14 @@ void fsm_msgSolanaSignMessage(const SolanaSignMessage *msg) { uint8_t public_key[32]; ed25519_publickey(node->private_key, public_key); - // Confirm message signing with user (shows message preview) - // Default to showing confirmation when client doesn't set the field - if (!msg->has_show_display || msg->show_display) { - if (!solana_confirmMessage(msg->message.bytes, msg->message.size)) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, - _("Message signing cancelled")); - layoutHome(); - return; - } + // Always require user confirmation — signed messages can authorize + // on-chain actions. Never allow host to bypass via show_display=false. + if (!solana_confirmMessage(msg->message.bytes, msg->message.size)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Message signing cancelled")); + layoutHome(); + return; } // Sign the message diff --git a/lib/firmware/fsm_msg_ton.h b/lib/firmware/fsm_msg_ton.h index 528cb0404..49ce8763a 100644 --- a/lib/firmware/fsm_msg_ton.h +++ b/lib/firmware/fsm_msg_ton.h @@ -115,15 +115,14 @@ void fsm_msgTonSignTx(TonSignTx *msg) { return; } - bool needs_confirm = true; - - // Display transaction details if available - if (needs_confirm && msg->has_to_address && msg->has_amount) { + // TON uses Cell/BoC encoding which cannot be parsed on-device. + // Display host-supplied fields with explicit blind-sign warning. + if (msg->has_to_address && msg->has_amount) { char amount_str[32]; ton_formatAmount(amount_str, sizeof(amount_str), msg->amount); if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, - "Send", "Send %s TON to %s?", + "TON Transfer", "Send %s to\n%s?", amount_str, msg->to_address)) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); @@ -132,8 +131,9 @@ void fsm_msgTonSignTx(TonSignTx *msg) { } } - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TON transaction?")) { + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Blind Signature", + "TON TX details cannot be verified on device. " + "Sign only if you trust the sending app.")) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 7e54ebb30..14758c6ec 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -105,16 +105,36 @@ void fsm_msgTronSignTx(TronSignTx *msg) { return; } - bool needs_confirm = true; + // Try to parse a TransferContract from the signed payload so the device + // can show verified transfer details instead of host-asserted fields. + TronParsedTransfer parsed; + if (tron_parseTransfer(msg->raw_data.bytes, msg->raw_data.size, &parsed)) { + // Simple TRX transfer — show on-device-verified details + char to_addr[TRON_ADDRESS_MAX_LEN]; + if (!base58_encode_check(parsed.to_address, 21, HASHER_SHA2D, + to_addr, sizeof(to_addr))) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, _("Bad to_address in TX")); + 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); + tron_formatAmount(amount_str, sizeof(amount_str), (uint64_t)parsed.amount); if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, - "Send", "Send %s TRX to %s?", - amount_str, msg->to_address)) { + "TRON Transfer", "Send %s to\n%s?", + amount_str, to_addr)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); + layoutHome(); + return; + } + } else { + // Token / contract / complex TX — blind sign with explicit warning + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Blind Signature", + "Cannot verify TX details on device. " + "Sign only if you trust the sending app.")) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); @@ -122,8 +142,8 @@ void fsm_msgTronSignTx(TronSignTx *msg) { } } - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TRON transaction?")) { + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Confirm", + "Sign this TRON transaction?")) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); diff --git a/lib/firmware/solana_tx.c b/lib/firmware/solana_tx.c index dbaed9815..7db049c0b 100644 --- a/lib/firmware/solana_tx.c +++ b/lib/firmware/solana_tx.c @@ -108,6 +108,7 @@ bool solana_parseTransaction(const uint8_t *raw_tx, size_t tx_size, uint16_t num_instructions; if (!read_compact_u16(&data, &remaining, &num_instructions)) return false; parsed->num_instructions = MIN(num_instructions, 8); + parsed->total_instructions = num_instructions; for (int i = 0; i < num_instructions; i++) { // Use a stack variable for instructions beyond our storage limit @@ -313,16 +314,18 @@ bool solana_confirmTransaction(const SolanaParsedTransaction *tx, return false; } - // For now, handle simple single-instruction transactions const SolanaInstruction *instr = &tx->instructions[0]; - if (instr->type == SOLANA_INSTRUCTION_SYSTEM_TRANSFER) { + // Only show verified transfer details for single-instruction system + // transfers. Multi-instruction TXs could hide malicious instructions + // behind a benign first one — the user must be warned. + if (tx->total_instructions == 1 && + instr->type == SOLANA_INSTRUCTION_SYSTEM_TRANSFER) { SolanaSystemTransfer transfer; if (!solana_parseSystemTransfer(instr->data, instr->data_len, &transfer)) { return false; } - // Get recipient address (account index 1 for System Transfer) if (instr->num_accounts < 2) return false; uint8_t to_idx = instr->account_indices[1]; if (to_idx >= tx->num_accounts) return false; @@ -342,9 +345,11 @@ bool solana_confirmTransaction(const SolanaParsedTransaction *tx, amount_str, to_address); } - // Unknown or complex transaction - show warning + // Multi-instruction or unknown program — blind sign with warning return confirm(ButtonRequestType_ButtonRequest_SignTx, - "Solana Transaction", - "Sign transaction with %d instruction(s)?", - tx->num_instructions); + "Blind Signature", + "TX has %d instruction(s) that cannot be " + "fully verified on device. Sign only if " + "you trust the sending app.", + tx->total_instructions); } diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index cbd5b0b4b..4303fecc0 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -80,6 +80,168 @@ void tron_formatAmount(char *buf, size_t len, uint64_t amount) { bn_format(&val, NULL, " TRX", TRON_DECIMALS, 0, false, buf, len); } +/* + * Minimal protobuf wire-format parser for TRON TransferContract. + * + * TRON Transaction.raw layout (protobuf): + * field 11 (contract, repeated message) → Contract { + * field 1 (type, enum): ContractType (1 = TransferContract) + * field 2 (parameter, Any message) → google.protobuf.Any { + * field 1 (type_url, string) + * field 2 (value, bytes) → serialized TransferContract { + * field 1 (owner_address, bytes): 21 bytes + * field 2 (to_address, bytes): 21 bytes + * field 3 (amount, int64): varint + * } + * } + * } + */ + +// Read a protobuf varint, return bytes consumed (0 on error) +static size_t pb_read_varint(const uint8_t *buf, size_t len, uint64_t *val) { + *val = 0; + size_t i = 0; + unsigned shift = 0; + while (i < len && shift < 64) { + uint64_t byte = buf[i]; + *val |= (byte & 0x7F) << shift; + shift += 7; + i++; + if (!(byte & 0x80)) return i; + } + return 0; // malformed +} + +// Skip a protobuf field given its wire type (0=varint, 2=length-delimited, etc) +static size_t pb_skip_field(const uint8_t *buf, size_t len, unsigned wire_type) { + if (wire_type == 0) { // varint + uint64_t dummy; + return pb_read_varint(buf, len, &dummy); + } else if (wire_type == 2) { // length-delimited + uint64_t flen; + size_t n = pb_read_varint(buf, len, &flen); + if (n == 0 || n + flen > len) return 0; + return n + (size_t)flen; + } else if (wire_type == 5) { // 32-bit + return len >= 4 ? 4 : 0; + } else if (wire_type == 1) { // 64-bit + return len >= 8 ? 8 : 0; + } + return 0; +} + +// Find a length-delimited field by field number in a protobuf message. +// Returns pointer to the field data and sets *out_len. NULL if not found. +static const uint8_t *pb_find_bytes(const uint8_t *buf, size_t len, + unsigned field_num, size_t *out_len) { + size_t pos = 0; + while (pos < len) { + uint64_t tag; + size_t n = pb_read_varint(buf + pos, len - pos, &tag); + if (n == 0) return NULL; + pos += n; + unsigned fn = (unsigned)(tag >> 3); + unsigned wt = (unsigned)(tag & 7); + if (fn == field_num && wt == 2) { + uint64_t flen; + n = pb_read_varint(buf + pos, len - pos, &flen); + if (n == 0 || pos + n + flen > len) return NULL; + *out_len = (size_t)flen; + return buf + pos + n; + } + n = pb_skip_field(buf + pos, len - pos, wt); + if (n == 0) return NULL; + pos += n; + } + return NULL; +} + +// Find a varint field by field number. Returns true if found. +static bool pb_find_varint(const uint8_t *buf, size_t len, + unsigned field_num, uint64_t *val) { + size_t pos = 0; + while (pos < len) { + uint64_t tag; + size_t n = pb_read_varint(buf + pos, len - pos, &tag); + if (n == 0) return false; + pos += n; + unsigned fn = (unsigned)(tag >> 3); + unsigned wt = (unsigned)(tag & 7); + if (fn == field_num && wt == 0) { + n = pb_read_varint(buf + pos, len - pos, val); + return n > 0; + } + n = pb_skip_field(buf + pos, len - pos, wt); + if (n == 0) return false; + pos += n; + } + return false; +} + +// Count occurrences of a length-delimited field in a protobuf message. +static unsigned pb_count_field(const uint8_t *buf, size_t len, + unsigned field_num) { + unsigned count = 0; + size_t pos = 0; + while (pos < len) { + uint64_t tag; + size_t n = pb_read_varint(buf + pos, len - pos, &tag); + if (n == 0) break; + pos += n; + unsigned fn = (unsigned)(tag >> 3); + unsigned wt = (unsigned)(tag & 7); + if (fn == field_num && wt == 2) count++; + n = pb_skip_field(buf + pos, len - pos, wt); + if (n == 0) break; + pos += n; + } + return count; +} + +bool tron_parseTransfer(const uint8_t *raw_data, size_t raw_data_len, + TronParsedTransfer *out) { + memset(out, 0, sizeof(*out)); + + // Reject if there are multiple contract entries (repeated field 11). + // A malicious payload could hide extra contracts after a benign transfer. + if (pb_count_field(raw_data, raw_data_len, 11) != 1) return false; + + // Find the single contract entry + size_t contract_len = 0; + const uint8_t *contract = pb_find_bytes(raw_data, raw_data_len, 11, + &contract_len); + if (!contract) return false; + + // Check contract type (field 1): must be 1 (TransferContract) + uint64_t contract_type = 0; + if (!pb_find_varint(contract, contract_len, 1, &contract_type)) return false; + if (contract_type != 1) return false; // not a simple transfer + + // Find parameter (field 2) = google.protobuf.Any + size_t any_len = 0; + const uint8_t *any = pb_find_bytes(contract, contract_len, 2, &any_len); + if (!any) return false; + + // Find value (field 2 inside Any) = serialized TransferContract + size_t tc_len = 0; + const uint8_t *tc = pb_find_bytes(any, any_len, 2, &tc_len); + if (!tc) return false; + + // Parse TransferContract: field 2 = to_address (bytes, 21) + size_t addr_len = 0; + const uint8_t *to_addr = pb_find_bytes(tc, tc_len, 2, &addr_len); + if (!to_addr || addr_len != 21) return false; + memcpy(out->to_address, to_addr, 21); + + // Parse TransferContract: field 3 = amount (varint, signed zigzag or plain) + uint64_t amount_raw = 0; + if (!pb_find_varint(tc, tc_len, 3, &amount_raw)) return false; + out->amount = (int64_t)amount_raw; + + out->valid = true; + return true; +} + /** * Sign a TRON transaction with secp256k1 */