Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/keepkey/firmware/ton.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/firmware/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ set(sources
signing.c
storage.c
tiny-json.c
tron.c
ton.c
transaction.c
txin_check.c
u2f.c)
Expand Down
52 changes: 44 additions & 8 deletions lib/firmware/fsm_msg_ton.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions lib/firmware/fsm_msg_tron.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
58 changes: 58 additions & 0 deletions lib/firmware/ton.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions unittests/firmware/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ set(sources
coins.cpp
recovery.cpp
storage.cpp
ton.cpp
usb_rx.cpp
u2f.cpp)

Expand Down
115 changes: 115 additions & 0 deletions unittests/firmware/ton.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
extern "C" {
#include "keepkey/firmware/ton.h"
#include <string.h>
}

#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");
}
Loading