Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/features/set_external_plugin/cmd_set_external_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ uint16_t handle_set_external_plugin(const uint8_t *workBuffer, uint8_t dataLengt
workBuffer += ADDRESS_LENGTH;
memmove(dataContext.tokenContext.methodSelector, workBuffer, SELECTOR_SIZE);
pluginType = PLUGIN_TYPE_EXTERNAL;
// External-plugin registration is not bound to a chain (no chain_id in the
// signed payload), so we mark it ANY here. Cross-chain replay protection
// for external plugins requires extending the signed payload to include
// chain_id, which is a protocol change outside the scope of this fix.
dataContext.tokenContext.pluginChainId = PLUGIN_CHAIN_ID_ANY;

return SWO_SUCCESS;
}
5 changes: 5 additions & 0 deletions src/features/set_plugin/cmd_set_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ uint16_t handle_set_plugin(const uint8_t *workBuffer, uint8_t dataLength) {
UNSUPPORTED_CHAIN_ID_MSG(chain_id);
return SWO_INCORRECT_DATA;
}
// Bind the registration to the signed chain. Without this, a host could
// load a valid signed plugin registration for one EVM chain and then have
// the device activate the plugin UI for a transaction on a different
// chain where the same address/selector means something else.
tokenContext->pluginChainId = chain_id;
offset += CHAIN_ID_SIZE;

keyId = workBuffer[offset];
Expand Down
15 changes: 15 additions & 0 deletions src/features/sign_authorization_eip7702/commands_7702.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,28 @@ uint16_t handle_sign_eip7702_authorization(uint8_t p1,
uint8_t dataLength) {
g_7702_sw = SWO_PARAMETER_ERROR_NO_INFO;
if (p1 == P1_FIRST_CHUNK) {
// Lock the EIP-7702 authorization flow against being restarted while
// another signing/review is in progress. Without this guard, a hostile
// host could overwrite tmpCtx.authSigningContext7702.bip32 during a
// pending review and trick the user into signing with a path other
// than the one displayed on screen.
if (appState != APP_STATE_IDLE) {
PRINTF("Cannot start an EIP-7702 authorization while another flow is active\n");
return SWO_COMMAND_NOT_ALLOWED;
}
appState = APP_STATE_SIGNING_EIP7702;
if ((dataBuffer =
parseBip32(dataBuffer, &dataLength, &tmpCtx.authSigningContext7702.bip32)) ==
NULL) {
reset_app_context();
return SWO_INCORRECT_DATA;
}
} else if (appState != APP_STATE_SIGNING_EIP7702) {
PRINTF("EIP-7702 continuation chunk without an active authorization session\n");
return SWO_COMMAND_NOT_ALLOWED;
}
if (!tlv_from_apdu(p1 == P1_FIRST_CHUNK, dataLength, dataBuffer, &handle_auth7702_tlv)) {
reset_app_context();
return g_7702_sw;
}
return SWO_NO_RESPONSE;
Expand Down
4 changes: 3 additions & 1 deletion src/features/sign_authorization_eip7702/ui_common_7702.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ unsigned int auth_7702_ok_cb(void) {
} else {
G_io_tx_buffer[0] = 0;
}
return io_seproxyhal_send_status(SWO_SUCCESS, ECDSA_SIGNATURE_LENGTH, false, true);
// Reset to release the APP_STATE_SIGNING_EIP7702 lock and clear the
// signing context so a new authorization can start from a clean slate.
return io_seproxyhal_send_status(SWO_SUCCESS, ECDSA_SIGNATURE_LENGTH, true, true);
}

unsigned int auth_7702_cancel_cb(void) {
Expand Down
13 changes: 13 additions & 0 deletions src/features/sign_tx/logic_sign_tx.c
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ __attribute__((noinline)) static uint16_t finalize_parsing_helper(const txContex
goto end;
}
PRINTF("FROM address displayed: %s\n", strings.common.fromAddress);
// Enforce chain binding for plugin registrations now that the full tx has
// been parsed and get_tx_chain_id() is reliable (LEGACY transactions only
// expose chain_id through the V field, which is parsed after the plugin
// init triggered on the data field).
if ((dataContext.tokenContext.pluginStatus >= ETH_PLUGIN_RESULT_SUCCESSFUL) &&
(dataContext.tokenContext.pluginChainId != PLUGIN_CHAIN_ID_ANY) &&
(dataContext.tokenContext.pluginChainId != chain_id)) {
PRINTF("Plugin registered for chain %llu but tx is on chain %llu\n",
dataContext.tokenContext.pluginChainId,
chain_id);
report_finalize_error();
return SWO_NO_RESPONSE;
}
// Finalize the plugin handling
if (dataContext.tokenContext.pluginStatus >= ETH_PLUGIN_RESULT_SUCCESSFUL) {
eth_plugin_prepare_finalize(&pluginFinalize);
Expand Down
13 changes: 10 additions & 3 deletions src/nbgl/ui_home.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,21 @@ static void prepare_and_display_home(const char *appname, const char *tagline, u
*/
static void get_appname_and_tagline(const char **appname, const char **tagline) {
uint64_t mainnet_chain_id;
uint8_t line_len = 1; // Initialize lengths to 1 for '\0' character
// Cap caller-provided plugin name length. 64 is generous compared to real
// Ledger plugin app names (typically < 20 chars) and the fuzz harnesses'
// NAME_LENGTH=32, while staying well below the byte that would let the
// total line length wrap. Any value below ~196 closes the bug.
const size_t max_name_len = 64;

if (caller_app) {
*appname = caller_app->name;

if (caller_app->type == CALLER_TYPE_PLUGIN) {
line_len += strlen(FORMAT_PLUGIN);
line_len += strlen(caller_app->name);
size_t name_len = strnlen(caller_app->name, max_name_len + 1);
if (name_len > max_name_len) {
return;
}
size_t line_len = 1 + strlen(FORMAT_PLUGIN) + name_len;
// Allocate the buffer - will never be deallocated...
if (APP_MEM_CALLOC((void **) &g_tag_line, line_len) == true) {
snprintf(g_tag_line, line_len, FORMAT_PLUGIN, *appname);
Expand Down
58 changes: 52 additions & 6 deletions src/plugins/eip7002/eip7002_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,31 @@ static void eip7002_plugin_provider_parameter(ethPluginProvideParameter_t *param
}
}

// Whether the transaction carries a non-zero native value that the user must see.
static bool has_tx_value(const ethPluginFinalize_t *param) {
return (param->txContent != NULL) && (param->txContent->value.length != 0);
}

// Same check using the QUERY_CONTRACT_UI message, which exposes txContent on
// a different struct.
static bool has_tx_value_ui(const ethQueryContractUI_t *param) {
return (param->txContent != NULL) && (param->txContent->value.length != 0);
}

static void eip7002_plugin_finalize(ethPluginFinalize_t *param) {
eip7002_context_t *context = (eip7002_context_t *) param->pluginContext;

param->uiType = ETH_UI_TYPE_GENERIC;
param->numScreens = is_zeroes_buffer(context->raw_amount, sizeof(context->raw_amount)) ? 1 : 2;
// Validator screen is always shown. The native tx.value is shown whenever
// non-zero so a hostile dApp cannot smuggle ETH/native value into a
// staking-style request that otherwise only renders calldata.
param->numScreens = 1;
if (has_tx_value(param)) {
param->numScreens++;
}
if (!is_zeroes_buffer(context->raw_amount, sizeof(context->raw_amount))) {
param->numScreens++;
}
param->result = (context->received == sizeof(context->withdrawal_request))
? ETH_PLUGIN_RESULT_OK
: ETH_PLUGIN_RESULT_ERROR;
Expand All @@ -88,9 +108,23 @@ static void eip7002_plugin_query_contract_ui(ethQueryContractUI_t *param) {
eip7002_context_t *context = (eip7002_context_t *) param->pluginContext;
uint64_t chain_id = get_tx_chain_id();
const char *ticker = get_displayable_ticker(&chain_id, g_chain_config, true);
// Map a screen index to a logical screen kind based on which optional
// screens are present for this transaction.
bool show_tx_value = has_tx_value_ui(param);
bool show_request_amount = !is_zeroes_buffer(context->raw_amount, sizeof(context->raw_amount));
uint8_t idx = param->screenIndex;
enum { S_VALIDATOR, S_TX_VALUE, S_REQUEST_AMOUNT, S_UNKNOWN } screen = S_UNKNOWN;

if (idx == 0) {
screen = S_VALIDATOR;
} else if (show_tx_value && idx == 1) {
screen = S_TX_VALUE;
} else if (show_request_amount && idx == (show_tx_value ? 2 : 1)) {
screen = S_REQUEST_AMOUNT;
}

switch (param->screenIndex) {
case 0:
switch (screen) {
case S_VALIDATOR:
if (param->msgLength < 2) {
return;
}
Expand All @@ -101,7 +135,19 @@ static void eip7002_plugin_query_contract_ui(ethQueryContractUI_t *param) {
&param->msg[2],
param->msgLength - 2);
break;
case 1:
case S_TX_VALUE:
strlcpy(param->title, "Tx value", param->titleLength);
if (!amountToString(param->txContent->value.value,
param->txContent->value.length,
WEI_TO_ETHER,
ticker,
param->msg,
param->msgLength)) {
param->result = ETH_PLUGIN_RESULT_ERROR;
return;
}
break;
case S_REQUEST_AMOUNT:
strlcpy(param->title, "Amount", param->titleLength);
if (!amountToString(context->raw_amount,
sizeof(context->raw_amount),
Expand All @@ -110,10 +156,10 @@ static void eip7002_plugin_query_contract_ui(ethQueryContractUI_t *param) {
param->msg,
param->msgLength)) {
param->result = ETH_PLUGIN_RESULT_ERROR;
break;
return;
}
break;
default:
case S_UNKNOWN:
break;
}
param->result = ETH_PLUGIN_RESULT_OK;
Expand Down
96 changes: 71 additions & 25 deletions src/plugins/eip7251/eip7251_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,30 @@ static void eip7251_plugin_provider_parameter(ethPluginProvideParameter_t *param
}
}

// Whether the transaction carries a non-zero native value that the user must see.
static bool has_tx_value(const ethPluginFinalize_t *param) {
return (param->txContent != NULL) && (param->txContent->value.length != 0);
}

static bool has_tx_value_ui(const ethQueryContractUI_t *param) {
return (param->txContent != NULL) && (param->txContent->value.length != 0);
}

static void eip7251_plugin_finalize(ethPluginFinalize_t *param) {
eip7251_context_t *context = (eip7251_context_t *) param->pluginContext;

param->uiType = ETH_UI_TYPE_GENERIC;
param->numScreens = target_equals_source(context) ? 1 : 2;
// Source validator is always shown. Target is shown when distinct. The
// native tx.value is shown whenever non-zero so a hostile dApp cannot
// smuggle ETH/native value into a consolidation request that otherwise
// only renders validator pubkeys.
param->numScreens = 1;
if (!target_equals_source(context)) {
param->numScreens++;
}
if (has_tx_value(param)) {
param->numScreens++;
}
param->result = (context->received == sizeof(context->consolidation_request))
? ETH_PLUGIN_RESULT_OK
: ETH_PLUGIN_RESULT_ERROR;
Expand All @@ -86,33 +105,60 @@ static void eip7251_plugin_query_contract_id(ethQueryContractID_t *param) {

static void eip7251_plugin_query_contract_ui(ethQueryContractUI_t *param) {
eip7251_context_t *context = (eip7251_context_t *) param->pluginContext;
// Map a screen index to a logical screen kind based on which optional
// screens are present for this transaction.
bool show_target = !target_equals_source(context);
bool show_tx_value = has_tx_value_ui(param);
uint8_t idx = param->screenIndex;
enum { S_SOURCE, S_TARGET, S_TX_VALUE, S_UNKNOWN } screen = S_UNKNOWN;

if (idx == 0) {
screen = S_SOURCE;
} else if (show_target && idx == 1) {
screen = S_TARGET;
} else if (show_tx_value && idx == (show_target ? 2 : 1)) {
screen = S_TX_VALUE;
}

if (param->msgLength >= 2) {
memcpy(param->msg, "0x", 2);
switch (param->screenIndex) {
case 0:
if (target_equals_source(context)) {
strlcpy(param->title, "Validator", param->titleLength);
} else {
strlcpy(param->title, "From validator", param->titleLength);
}
format_hex(context->source_pubkey,
sizeof(context->source_pubkey),
&param->msg[2],
param->msgLength - 2);
break;
case 1:
strlcpy(param->title, "To validator", param->titleLength);
format_hex(context->target_pubkey,
sizeof(context->target_pubkey),
&param->msg[2],
param->msgLength - 2);
break;
default:
break;
if (param->msgLength < 2) {
return;
}
switch (screen) {
case S_SOURCE:
memcpy(param->msg, "0x", 2);
strlcpy(param->title, show_target ? "From validator" : "Validator", param->titleLength);
format_hex(context->source_pubkey,
sizeof(context->source_pubkey),
&param->msg[2],
param->msgLength - 2);
break;
case S_TARGET:
memcpy(param->msg, "0x", 2);
strlcpy(param->title, "To validator", param->titleLength);
format_hex(context->target_pubkey,
sizeof(context->target_pubkey),
&param->msg[2],
param->msgLength - 2);
break;
case S_TX_VALUE: {
uint64_t chain_id = get_tx_chain_id();
const char *ticker = get_displayable_ticker(&chain_id, g_chain_config, true);
strlcpy(param->title, "Tx value", param->titleLength);
if (!amountToString(param->txContent->value.value,
param->txContent->value.length,
WEI_TO_ETHER,
ticker,
param->msg,
param->msgLength)) {
param->result = ETH_PLUGIN_RESULT_ERROR;
return;
}
break;
}
param->result = ETH_PLUGIN_RESULT_OK;
case S_UNKNOWN:
return;
}
param->result = ETH_PLUGIN_RESULT_OK;
}

void eip7251_plugin_call(eth_plugin_msg_t msg, void *param) {
Expand Down
8 changes: 7 additions & 1 deletion src/plugins/erc1155/erc1155_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ void handle_finalize_1155(ethPluginFinalize_t *msg) {
msg->numScreens = 5;
break;
case SAFE_BATCH_TRANSFER:
msg->numScreens = 4;
// To, Collection Name, NFT Address, Total Quantity
// + 2 screens per displayed pair (ID + Quantity)
// + 1 warning screen if truncated.
msg->numScreens = 4 + 2 * context->batch_displayed;
if (context->batch_truncated) {
msg->numScreens += 1;
}
break;
case SET_APPROVAL_FOR_ALL:
msg->numScreens = 3;
Expand Down
14 changes: 14 additions & 0 deletions src/plugins/erc1155/erc1155_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

// Internal plugin for EIP 1155: https://eips.ethereum.org/EIPS/eip-1155

// Maximum number of (id, value) pairs surfaced individually during a
// safeBatchTransferFrom review. Anything beyond this is reported via the
// aggregate quantity screen plus a truncation warning so the user is told
// the on-device view is incomplete.
#define ERC1155_BATCH_DISPLAY_MAX 3

typedef struct erc1155_context_t {
uint8_t address[ADDRESS_LENGTH];
uint8_t tokenId[INT256_LENGTH];
Expand All @@ -22,6 +28,14 @@ typedef struct erc1155_context_t {
bool approved;
uint8_t next_param;
uint8_t selectorIndex;

// SAFE_BATCH_TRANSFER review data. Without these, the UI only displayed
// an aggregate "<total> from <count> NFT IDs" line and gave the user no
// way to detect a high-value token ID hidden among innocuous ones.
uint8_t batch_ids[ERC1155_BATCH_DISPLAY_MAX][INT256_LENGTH];
uint8_t batch_values[ERC1155_BATCH_DISPLAY_MAX][INT256_LENGTH];
uint8_t batch_displayed;
bool batch_truncated;
} erc1155_context_t;

void handle_provide_parameter_1155(ethPluginProvideParameter_t *parameters);
Expand Down
16 changes: 15 additions & 1 deletion src/plugins/erc1155/erc1155_provide_parameters.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,20 @@ static void handle_batch_transfer(ethPluginProvideParameter_t *msg, erc1155_cont
}
context->ids_array_len =
U2BE(msg->parameter, PARAMETER_LENGTH - sizeof(context->ids_array_len));
context->batch_displayed = (context->ids_array_len > ERC1155_BATCH_DISPLAY_MAX)
? ERC1155_BATCH_DISPLAY_MAX
: (uint8_t) context->ids_array_len;
context->batch_truncated = context->ids_array_len > ERC1155_BATCH_DISPLAY_MAX;
context->next_param = TOKEN_ID;
// set to zero for next step
context->array_index = 0;
break;
case TOKEN_ID:
// don't copy anything since we won't display it
// Surface the first batch entries to the user so a malicious
// batch cannot hide a high-value token ID behind innocuous ones.
if (context->array_index < ERC1155_BATCH_DISPLAY_MAX) {
memcpy(context->batch_ids[context->array_index], msg->parameter, INT256_LENGTH);
}
if (--context->ids_array_len == 0) {
context->next_param = VALUE_LENGTH;
}
Expand Down Expand Up @@ -98,6 +106,12 @@ static void handle_batch_transfer(ethPluginProvideParameter_t *msg, erc1155_cont
case VALUE:
// put it temporarily in token id since we don't use it in batch transfer
copy_parameter(context->tokenId, msg->parameter, sizeof(context->value));
// Keep the first per-id values so they pair up with batch_ids
// entries on screen. Anything past ERC1155_BATCH_DISPLAY_MAX is
// still aggregated into the total below.
if (context->array_index < ERC1155_BATCH_DISPLAY_MAX) {
memcpy(context->batch_values[context->array_index], msg->parameter, INT256_LENGTH);
}
convertUint256BE(context->tokenId, sizeof(context->tokenId), &new_value);
add256(&context->value, &new_value, &context->value);
if (--context->values_array_len == 0) {
Expand Down
Loading
Loading