diff --git a/deps/device-protocol b/deps/device-protocol index ace5a7074..b07589ff3 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit ace5a70745e9948d1949ca78b0249adb75b00e73 +Subproject commit b07589ff386265f894736a490fc163d46f9479b7 diff --git a/include/keepkey/firmware/ton.h b/include/keepkey/firmware/ton.h index 3bcd8224e..0686a36a5 100644 --- a/include/keepkey/firmware/ton.h +++ b/include/keepkey/firmware/ton.h @@ -49,6 +49,14 @@ bool ton_get_address(const ed25519_public_key public_key, bool bounceable, size_t address_len, char *raw_address, size_t raw_address_len); +/** + * Validate a TON user-friendly address (Base64 URL-safe with CRC16). + * Decodes the address, checks CRC16-XMODEM checksum, verifies tag byte. + * @param address Base64 URL-safe encoded TON address (48 chars) + * @return true if address is valid + */ +bool ton_validateAddress(const char *address); + /** * Format TON amount (nanoTON) for display * @param buf Output buffer diff --git a/lib/firmware/CMakeLists.txt b/lib/firmware/CMakeLists.txt index d20b79bdf..c40cca602 100644 --- a/lib/firmware/CMakeLists.txt +++ b/lib/firmware/CMakeLists.txt @@ -14,6 +14,8 @@ set(sources signing.c storage.c tiny-json.c + tron.c + ton.c transaction.c txin_check.c u2f.c) diff --git a/lib/firmware/fsm_msg_ton.h b/lib/firmware/fsm_msg_ton.h index 528cb0404..64966e5ea 100644 --- a/lib/firmware/fsm_msg_ton.h +++ b/lib/firmware/fsm_msg_ton.h @@ -45,6 +45,15 @@ void fsm_msgTonGetAddress(const TonGetAddress *msg) { bool testnet = msg->has_testnet ? msg->testnet : false; int32_t workchain = msg->has_workchain ? msg->workchain : 0; + // Restrict workchain to valid values: 0 (basechain) or -1 (masterchain) + if (workchain != 0 && workchain != -1) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Workchain must be 0 or -1")); + layoutHome(); + return; + } + // Get TON address from public key (Base64 URL-safe encoding) char address[MAX_ADDR_SIZE]; char raw_address[MAX_ADDR_SIZE]; @@ -101,6 +110,7 @@ void fsm_msgTonSignTx(TonSignTx *msg) { return; } + // Derive node using Ed25519 curve HDNode *node = fsm_getDerivedNode(ED25519_NAME, msg->address_n, msg->address_n_count, NULL); @@ -115,27 +125,53 @@ void fsm_msgTonSignTx(TonSignTx *msg) { return; } - bool needs_confirm = true; + // Restrict workchain to valid values if provided + if (msg->has_workchain && msg->workchain != 0 && msg->workchain != -1) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Workchain must be 0 or -1")); + layoutHome(); + return; + } + + // TON uses Cell/BoC encoding which cannot be parsed on-device. + // Display host-supplied fields with explicit blind-sign warning. - // Display transaction details if available - if (needs_confirm && msg->has_to_address && msg->has_amount) { + // Validate destination address if provided (independent of amount) + if (msg->has_to_address) { + if (!ton_validateAddress(msg->to_address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid TON destination address")); + layoutHome(); + return; + } + } + + // Show transfer details if both destination and amount are provided + 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"); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); layoutHome(); return; } } - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TON transaction?")) { + // Always require explicit blind-sign acknowledgment — TON Cell/BoC + // encoding cannot be verified on-device + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Blind Signature", + "TON TX details cannot be\nverified on device.\n" + "Sign only if you trust\nthe sending app.")) { memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); layoutHome(); return; } diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 7e54ebb30..beea2f769 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -91,6 +91,7 @@ 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); diff --git a/lib/firmware/ton.c b/lib/firmware/ton.c index 7dbc0181a..6b9cfb1e9 100644 --- a/lib/firmware/ton.c +++ b/lib/firmware/ton.c @@ -214,6 +214,64 @@ bool ton_get_address(const ed25519_public_key public_key, bool bounceable, return true; } +/** + * Decode Base64 URL-safe string to bytes. + * Returns decoded length, or -1 on error. + */ +static int base64_url_decode(const char *in, size_t in_len, + uint8_t *out, size_t out_cap) { + /* Build reverse lookup table */ + int8_t lut[128]; + memset(lut, -1, sizeof(lut)); + for (int i = 0; i < 64; i++) { + lut[(unsigned char)base64_url_alphabet[i]] = (int8_t)i; + } + + size_t op = 0; + uint32_t accum = 0; + int bits = 0; + for (size_t i = 0; i < in_len; i++) { + unsigned char c = (unsigned char)in[i]; + if (c >= 128 || lut[c] < 0) return -1; + accum = (accum << 6) | (uint32_t)lut[c]; + bits += 6; + if (bits >= 8) { + bits -= 8; + if (op >= out_cap) return -1; + out[op++] = (uint8_t)((accum >> bits) & 0xFF); + } + } + return (int)op; +} + +/** + * Validate a TON user-friendly address (Base64 URL-safe with CRC16-XMODEM). + * TON addresses are 48 chars Base64 → 36 bytes = [tag(1) + workchain(1) + + * hash(32) + crc16(2)]. + */ +bool ton_validateAddress(const char *address) { + if (!address) return false; + size_t len = strlen(address); + if (len != 48) return false; + + uint8_t decoded[36]; + int dlen = base64_url_decode(address, len, decoded, sizeof(decoded)); + if (dlen != 36) return false; + + /* Validate tag byte: bounceable=0x11, non-bounceable=0x51, + testnet variants have 0x80 set */ + uint8_t tag = decoded[0]; + uint8_t base_tag = tag & 0x7F; + if (base_tag != 0x11 && base_tag != 0x51) return false; + + /* Validate CRC16-XMODEM over first 34 bytes */ + uint16_t expected_crc = ((uint16_t)decoded[34] << 8) | decoded[35]; + uint16_t actual_crc = ton_crc16(decoded, 34); + if (expected_crc != actual_crc) return false; + + return true; +} + /** * Format TON amount (nanoTON) for display * 1 TON = 1,000,000,000 nanoTON diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt index 1f4ccddec..221580e16 100644 --- a/unittests/firmware/CMakeLists.txt +++ b/unittests/firmware/CMakeLists.txt @@ -2,6 +2,7 @@ set(sources coins.cpp recovery.cpp storage.cpp + ton.cpp usb_rx.cpp u2f.cpp) diff --git a/unittests/firmware/ton.cpp b/unittests/firmware/ton.cpp new file mode 100644 index 000000000..1fcdaf4d8 --- /dev/null +++ b/unittests/firmware/ton.cpp @@ -0,0 +1,115 @@ +extern "C" { +#include "keepkey/firmware/ton.h" +#include +} + +#include "gtest/gtest.h" + +/* ------------------------------------------------------------------ */ +/* Address validation tests */ +/* ------------------------------------------------------------------ */ + +TEST(Ton, ValidateGoodBounceable) { + // Generate a known address from the device and validate it. + // We test with a well-known TON address format (48 chars, base64url). + // Using UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA as a baseline + // format test — real addresses from ton_get_address will be validated + // in integration tests. + + // Test the validation function handles NULL + EXPECT_FALSE(ton_validateAddress(NULL)); +} + +TEST(Ton, ValidateWrongLength) { + // Too short + EXPECT_FALSE(ton_validateAddress("EQBvW8Z5h")); + // Too long + EXPECT_FALSE(ton_validateAddress( + "EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsILOAAAAAAAAAAAAA")); + // Empty + EXPECT_FALSE(ton_validateAddress("")); +} + +TEST(Ton, ValidateBadChecksum) { + // 48-char string but with garbage — CRC16 won't match + EXPECT_FALSE(ton_validateAddress( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCD")); +} + +TEST(Ton, ValidateBadTag) { + // 48-char base64url that decodes but has wrong tag byte + // Tag must be 0x11, 0x51, 0x91, or 0xD1 + // This is a string that decodes to 36 bytes with tag=0x00 + EXPECT_FALSE(ton_validateAddress( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")); +} + +/* ------------------------------------------------------------------ */ +/* Round-trip: generate address then validate it */ +/* ------------------------------------------------------------------ */ + +TEST(Ton, GenerateAndValidate) { + // Use a dummy 32-byte public key + uint8_t pubkey[32]; + memset(pubkey, 0x42, 32); + + char address[TON_ADDRESS_MAX_LEN]; + char raw_address[TON_RAW_ADDRESS_MAX_LEN]; + + ASSERT_TRUE(ton_get_address(pubkey, true, false, 0, + address, sizeof(address), + raw_address, sizeof(raw_address))); + + // Generated address must be 48 chars + EXPECT_EQ(strlen(address), 48u); + + // Generated address must pass validation + EXPECT_TRUE(ton_validateAddress(address)); +} + +TEST(Ton, GenerateNonBounceableAndValidate) { + uint8_t pubkey[32]; + memset(pubkey, 0xAB, 32); + + char address[TON_ADDRESS_MAX_LEN]; + char raw_address[TON_RAW_ADDRESS_MAX_LEN]; + + ASSERT_TRUE(ton_get_address(pubkey, false, false, 0, + address, sizeof(address), + raw_address, sizeof(raw_address))); + + EXPECT_EQ(strlen(address), 48u); + EXPECT_TRUE(ton_validateAddress(address)); +} + +TEST(Ton, GenerateTestnetAndValidate) { + uint8_t pubkey[32]; + memset(pubkey, 0xCD, 32); + + char address[TON_ADDRESS_MAX_LEN]; + char raw_address[TON_RAW_ADDRESS_MAX_LEN]; + + ASSERT_TRUE(ton_get_address(pubkey, true, true, 0, + address, sizeof(address), + raw_address, sizeof(raw_address))); + + EXPECT_EQ(strlen(address), 48u); + EXPECT_TRUE(ton_validateAddress(address)); +} + +/* ------------------------------------------------------------------ */ +/* Formatting tests */ +/* ------------------------------------------------------------------ */ + +TEST(Ton, FormatAmount) { + char buf[64]; + + ton_formatAmount(buf, sizeof(buf), 1000000000ULL); // 1 TON + EXPECT_STREQ(buf, "1 TON"); + + ton_formatAmount(buf, sizeof(buf), 500000000ULL); // 0.5 TON + EXPECT_STREQ(buf, "0.5 TON"); + + ton_formatAmount(buf, sizeof(buf), 0); + EXPECT_STREQ(buf, "0 TON"); +}