Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4306b7a
Reject signing commands that arrive while a review is pending
cedelavergne-ledger Aug 6, 2026
6fa474f
Fix appState and idle handling around reset_app_context()
cedelavergne-ledger Aug 6, 2026
4ef7f4b
Pin the signing mode (P2) to the value set at P1_FIRST
cedelavergne-ledger Aug 6, 2026
de5058c
Reject P2 changes on TX signing continuation chunks
cedelavergne-ledger Aug 7, 2026
18a7870
Validate appState in signing approval callbacks
cedelavergne-ledger Aug 6, 2026
9443c49
Lock EIP-712 v1 preparatory APDUs against concurrent signing flows
cedelavergne-ledger Aug 6, 2026
5b7607c
Guard address verification against concurrent signing commands
cedelavergne-ledger Aug 6, 2026
b3b5e71
Extend address-verification guard to the ETH2 public key flow
cedelavergne-ledger Aug 6, 2026
505a2a1
Reject GCS string fields that are oversize or contain embedded NULs
cedelavergne-ledger Aug 7, 2026
b61fcbf
Guard privacy-operation and gating APDUs against concurrent flows
cedelavergne-ledger Aug 7, 2026
a944279
Introduce APP_STATE_PERFORMING_PRIVACY_OP for the privacy-op review
cedelavergne-ledger Aug 7, 2026
e0c4bc9
Fix GCS crash when a field APDU arrives during an active review
cedelavergne-ledger Aug 7, 2026
a795a5e
Fix unit tests broken by fail-closed APDU guards
cedelavergne-ledger Aug 7, 2026
0a4990e
Fix gating guard, field cleanup, and privacy-op test fixture
cedelavergne-ledger Aug 7, 2026
a9191ce
Restore batch page-break: hash-then-rollback with heap backup
cedelavergne-ledger Aug 10, 2026
15fd6bf
Guard plugin-set and TX-simulation APDUs against concurrent flows
cedelavergne-ledger Aug 13, 2026
6857f30
Fix double APDU response in plugin_ui_get_item_internal
cedelavergne-ledger Aug 10, 2026
f33373a
Fix strings.tmp aliasing in Safe Account UI
cedelavergne-ledger Aug 10, 2026
266624e
Guard handle_set_external_plugin against zero-length payload
cedelavergne-ledger Aug 10, 2026
704aa1c
Fix plugin context zeroing, erc1155 underflow, screen overflow, and m…
cedelavergne-ledger Aug 10, 2026
7dafff5
Fix out-of-bounds read in path_advance_in_struct (scan-build ArrayBound)
cedelavergne-ledger Aug 11, 2026
b9ea1ed
Fix multsig glyph
cedelavergne-ledger Aug 12, 2026
90b613f
Fix chain 78 icon
cedelavergne-ledger Aug 12, 2026
4220b58
Update snapshots
cedelavergne-ledger Aug 12, 2026
cb9230c
Bump version
cedelavergne-ledger Aug 12, 2026
d4b6308
use reusable_app_release.yml
mbrousset-ledger Apr 27, 2026
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
75 changes: 10 additions & 65 deletions .github/workflows/publish_release.yml
Original file line number Diff line number Diff line change
@@ -1,72 +1,17 @@
name: Build and publish release binaries

name: GitHub release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'

permissions:
contents: write
packages: read

jobs:
build_application:
name: Build application using the reusable workflow
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
github-release:
name: GitHub release using the reusable workflow
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_app_release.yml@v1
if: startsWith(github.ref, 'refs/tags/')
with:
upload_app_binaries_artifact: "release_elfs"

release:
name: Create GitHub Release with ELF binaries
needs: build_application
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download built binaries
uses: actions/download-artifact@v4
with:
name: release_elfs
path: binaries

- name: Checkout repository
uses: actions/checkout@v4

- name: Rename ELF files per device
run: |
mkdir release_assets
for device_dir in binaries/*/; do
device=$(basename "$device_dir")
if [ -f "${device_dir}bin/app.elf" ]; then
cp "${device_dir}bin/app.elf" "release_assets/app-${device}-${GITHUB_REF_NAME}.elf"
fi
done
ls -la release_assets/

- name: Extract release notes from CHANGELOG.md
id: changelog
run: |
VERSION="${GITHUB_REF_NAME}"
# Extract the section for this version from CHANGELOG.md
# Matches from "## [x.y.z]" until the next "## [" or end of file
NOTES=$(awk -v ver="$VERSION" '
BEGIN { found=0 }
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]")) { found=1; next }
}
found { print }
' CHANGELOG.md)

if [ -z "$NOTES" ]; then
echo "Warning: No changelog entry found for version $VERSION"
NOTES="Release $VERSION"
fi

# Write to file to avoid delimiter issues
echo "$NOTES" > release_notes.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \
--notes-file release_notes.md \
release_assets/*.elf
app_ref_name: ${{ github.ref_name }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.22.2](../../compare/1.22.1...1.22.2) - 2026-08-12

### Fixed

- Security issues

## [1.22.1](../../compare/1.22.0...1.22.1) - 2026-05-27

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ include ./makefile_conf/chain/$(CHAIN).mk

APPVERSION_M = 1
APPVERSION_N = 22
APPVERSION_P = 1
APPVERSION_P = 2
APPVERSION = $(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)

# Application source files
Expand Down
Binary file modified icons/nanox_app_chain_78.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 25 additions & 5 deletions src/features/generic_tx_parser/cmd_field.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "cmd_tx_info.h"
#include "gtp_tx_info.h"
#include "tx_ctx.h"
#include "app_mem_utils.h"

static bool handle_tlv_payload(const buffer_t *buf) {
s_field field = {0};
Expand All @@ -17,19 +18,35 @@ static bool handle_tlv_payload(const buffer_t *buf) {
cleanup_field_constraints(&field);
return false;
}
if (cx_hash_no_throw(get_fields_hash_ctx(), 0, buf->ptr, buf->size, NULL, 0) != CX_OK) {
PRINTF("Error: could not hash the field struct!\n");
if (!verify_field_struct(&ctx)) {
PRINTF("Error: could not verify the field struct!\n");
cleanup_field_constraints(&field);
return false;
}
if (!verify_field_struct(&ctx)) {
PRINTF("Error: could not verify the field struct!\n");
// Hash before format_field so validate_instruction_hash() inside
// add_to_field_table() sees the updated digest and sets end_intent on the
// last field of a batch sub-transaction (drives the NBGL page-break).
//
// Finding 907: rollback the hash if format_field() rejects the field so the
// rejected data leaves no trace in the signed digest.
cx_sha3_t *hash_ctx_backup;
if (APP_MEM_CALLOC((void **) &hash_ctx_backup, sizeof(*hash_ctx_backup)) == false) {
cleanup_field_constraints(&field);
return false;
}
memcpy(hash_ctx_backup, get_fields_hash_ctx(), sizeof(*hash_ctx_backup));
if (cx_hash_no_throw(get_fields_hash_ctx(), 0, buf->ptr, buf->size, NULL, 0) != CX_OK) {
PRINTF("Error: could not hash the field struct!\n");
APP_MEM_FREE(hash_ctx_backup);
cleanup_field_constraints(&field);
return false;
}
if (!format_field(&field)) {
memcpy(get_fields_hash_ctx(), hash_ctx_backup, sizeof(*hash_ctx_backup));
APP_MEM_FREE(hash_ctx_backup);
return false;
}
APP_MEM_FREE(hash_ctx_backup);
while (((appState == APP_STATE_SIGNING_EIP712) || !tx_ctx_is_root()) &&
validate_instruction_hash()) {
if (!process_empty_txs_after()) {
Expand All @@ -48,8 +65,11 @@ uint16_t handle_field(uint8_t p1, uint8_t p2, uint8_t lc, const uint8_t *payload
}

if (get_current_tx_info() == NULL) {
// Do NOT call gcs_cleanup() here: if a GCS review is currently shown,
// gcs_cleanup() frees g_pairs/g_pairsList while NBGL still holds
// pointers to them, causing a crash.
// Cleanup is handled in the approve/reject callbacks and in reset_app_context().
PRINTF("Error: Field received without a TX info!\n");
gcs_cleanup();
return SWO_COMMAND_NOT_ALLOWED;
}

Expand Down
13 changes: 12 additions & 1 deletion src/features/generic_tx_parser/gtp_param_raw.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,18 @@ static bool format_string(const s_value *def,
char *buf,
size_t buf_size) {
(void) def;
str_cpy_explicit_trunc((char *) value->ptr, value->length, buf, buf_size);
if (value->length + 1 > buf_size) {
PRINTF("RAW STRING value too long for display (%u > %u bytes)\n",
(unsigned) value->length + 1,
(unsigned) buf_size);
return false;
}
if (memchr(value->ptr, '\0', value->length) != NULL) {
PRINTF("RAW STRING value contains embedded NUL\n");
return false;
}
memmove(buf, value->ptr, value->length);
buf[value->length] = '\0';
return true;
}

Expand Down
5 changes: 4 additions & 1 deletion src/features/get_eth2_public_key/cmd_get_eth2_public_key.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ uint16_t handle_get_eth2_public_key(uint8_t p1,
cx_err_t error = CX_INTERNAL_ERROR;

if (!G_called_from_swap) {
reset_app_context();
if (appState != APP_STATE_IDLE) {
return SWO_COMMAND_NOT_ALLOWED;
}
}
if ((p1 != P1_CONFIRM) && (p1 != P1_NON_CONFIRM)) {
return SWO_WRONG_P1_P2;
Expand All @@ -90,6 +92,7 @@ uint16_t handle_get_eth2_public_key(uint8_t p1,
*tx = set_result_get_eth2_publicKey();
return SWO_SUCCESS;
}
appState = APP_STATE_VERIFYING_ADDRESS;
ui_display_public_eth2();
*flags |= IO_ASYNCH_REPLY;
// Return code will be sent after UI approve/cancel
Expand Down
5 changes: 4 additions & 1 deletion src/features/get_public_key/cmd_get_public_key.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ uint16_t handle_get_public_key(uint8_t p1,
cx_err_t error = CX_INTERNAL_ERROR;

if (!G_called_from_swap) {
reset_app_context();
if (appState != APP_STATE_IDLE) {
return SWO_COMMAND_NOT_ALLOWED;
}
}

if ((p1 != P1_CONFIRM) && (p1 != P1_NON_CONFIRM)) {
Expand Down Expand Up @@ -63,6 +65,7 @@ uint16_t handle_get_public_key(uint8_t p1,
"0x%.*s",
40,
tmpCtx.publicKeyContext.address);
appState = APP_STATE_VERIFYING_ADDRESS;
// don't unnecessarily pass the current app's chain ID
ui_display_public_key(chainConfig->chainId == chain_id ? NULL : &chain_id);
*flags |= IO_ASYNCH_REPLY;
Expand Down
3 changes: 3 additions & 0 deletions src/features/get_public_key/ui_common_get_public_key.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include "ui_callbacks.h"

unsigned int io_seproxyhal_touch_address_ok(void) {
if (appState != APP_STATE_VERIFYING_ADDRESS) {
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, false);
}
uint32_t tx = set_result_get_publicKey();
return io_seproxyhal_send_status(SWO_SUCCESS, tx, true, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ uint16_t handle_perform_privacy_operation(uint8_t p1,
bip32_path_t bip32;
cx_err_t error = CX_INTERNAL_ERROR;

if (appState != APP_STATE_IDLE) {
return SWO_COMMAND_NOT_ALLOWED;
}

if ((p1 != P1_CONFIRM) && (p1 != P1_NON_CONFIRM)) {
return SWO_WRONG_P1_P2;
}
Expand Down Expand Up @@ -102,6 +106,7 @@ uint16_t handle_perform_privacy_operation(uint8_t p1,
strings.common.fullAmount,
sizeof(strings.common.fullAmount) - 1);

appState = APP_STATE_PERFORMING_PRIVACY_OP;
if (p2 == P2_PUBLIC_ENCRYPTION_KEY) {
ui_display_privacy_public_key();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include "feature_perform_privacy_operation.h"

unsigned int io_seproxyhal_touch_privacy_ok(void) {
if (appState != APP_STATE_PERFORMING_PRIVACY_OP) {
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, false);
}
uint32_t tx = set_result_perform_privacy_operation();
return io_seproxyhal_send_status(SWO_SUCCESS, tx, true, true);
}
Expand Down
10 changes: 10 additions & 0 deletions src/features/provide_gating/cmd_get_gating.c
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,16 @@ static bool handle_tlv_payload(const buffer_t *buf) {
uint16_t handle_gating(uint8_t p1, uint8_t p2, const uint8_t *data, uint8_t length) {
uint16_t sw = SWO_PARAMETER_ERROR_NO_INFO;

switch (appState) {
case APP_STATE_IDLE:
case APP_STATE_SIGNING_TX:
case APP_STATE_SIGNING_EIP712:
case APP_STATE_PREPARING_EIP712:
break;
default:
return SWO_COMMAND_NOT_ALLOWED;
}

switch (p2) {
case 0x00:
if (!tlv_from_apdu(p1 == P1_FIRST_CHUNK, length, data, &handle_tlv_payload)) {
Expand Down
16 changes: 15 additions & 1 deletion src/features/provide_tx_simulation/cmd_get_tx_simulation.c
Original file line number Diff line number Diff line change
Expand Up @@ -470,14 +470,28 @@ uint16_t handle_tx_simulation(uint8_t p1,
sw = SWO_COMMAND_CODE_NOT_SUPPORTED;
break;
}
// Reject a second provisioning to prevent the host from overwriting
// the displayed warning after the review is on screen (finding 195).
// On master, tx_hash is zeroed by clear_tx_simulation() and set
// non-zero only after a successful TLV parse, so non-zero means received.
if (allzeroes((void *) TX_SIMULATION.tx_hash, HASH_SIZE) == 0) {
PRINTF("Error: TX simulation already received!\n");
sw = SWO_COMMAND_NOT_ALLOWED;
break;
}
if (!tlv_from_apdu(p2 == P1_FIRST_CHUNK, length, data, &handle_tlv_payload)) {
sw = SWO_INCORRECT_DATA;
} else {
sw = SWO_SUCCESS;
}
break;
case 0x01:
// TX Simulation Opt-In
// TX Simulation Opt-In: drawing a new screen while a review is on
// screen would overlay the live review (finding 199).
if (appState != APP_STATE_IDLE) {
sw = SWO_COMMAND_NOT_ALLOWED;
break;
}
handle_tx_simulation_opt_in(true);
*flags |= IO_ASYNCH_REPLY;
sw = APDU_NO_RESPONSE;
Expand Down
6 changes: 6 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 @@ -7,6 +7,12 @@

uint16_t handle_set_external_plugin(const uint8_t *workBuffer, uint8_t dataLength) {
PRINTF("Handling set Plugin\n");
if (appState != APP_STATE_IDLE) {
return SWO_COMMAND_NOT_ALLOWED;
}
if (dataLength < 1) {
return SWO_INCORRECT_DATA;
}
uint8_t hash[INT256_LENGTH];
uint8_t pluginNameLength = *workBuffer;
uint32_t params[2];
Expand Down
3 changes: 3 additions & 0 deletions src/features/set_plugin/cmd_set_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ void set_swap_with_calldata_plugin_type(void) {

uint16_t handle_set_plugin(const uint8_t *workBuffer, uint8_t dataLength) {
PRINTF("Handling set Plugin\n");
if (appState != APP_STATE_IDLE) {
return SWO_COMMAND_NOT_ALLOWED;
}
uint8_t hash[INT256_LENGTH] = {0};
tokenContext_t *tokenContext = &dataContext.tokenContext;
size_t offset = 0;
Expand Down
8 changes: 8 additions & 0 deletions src/features/sign_authorization_eip7702/commands_7702.c
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,16 @@ uint16_t handle_sign_eip7702_authorization(uint8_t p1,
}
}
if (!tlv_from_apdu(p1 == P1_FIRST_CHUNK, dataLength, dataBuffer, &handle_auth7702_tlv)) {
if (g_7702_sw == SWO_COMMAND_NOT_ALLOWED) {
// An error screen is already displayed; only reset the state so
// subsequent APDUs are accepted, the UI callback handles idle.
appState = APP_STATE_IDLE;
} else {
reset_app_context();
}
return g_7702_sw;
}
appState = APP_STATE_SIGNING_EIP7702;
*flags |= IO_ASYNCH_REPLY;
return APDU_NO_RESPONSE;
}
7 changes: 5 additions & 2 deletions src/features/sign_authorization_eip7702/ui_common_7702.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include "crypto_helpers.h"

unsigned int auth_7702_ok_cb(void) {
if (appState != APP_STATE_SIGNING_EIP7702) {
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, false);
}
uint32_t info = 0;
CX_ASSERT(bip32_derive_ecdsa_sign_rs_hash_256(CX_CURVE_256K1,
tmpCtx.authSigningContext7702.bip32.path,
Expand All @@ -21,9 +24,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);
return io_seproxyhal_send_status(SWO_SUCCESS, ECDSA_SIGNATURE_LENGTH, true, false);
}

unsigned int auth_7702_cancel_cb(void) {
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, true);
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, false);
}
3 changes: 1 addition & 2 deletions src/features/sign_message/cmd_sign_message.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ static const char SIGN_MAGIC[] =
*/
static void set_idle(void) {
message_cleanup();
APP_MEM_FREE_AND_NULL((void **) &g_msg_hash_ctx);
ui_idle();
}

Expand Down Expand Up @@ -232,7 +231,6 @@ uint16_t handle_sign_personal_message(uint8_t p1,
if (p1 == P1_FIRST) {
// Check if the app is in idle state
if (appState != APP_STATE_IDLE) {
set_idle();
return SWO_COMMAND_NOT_ALLOWED;
}
appState = APP_STATE_SIGNING_MESSAGE;
Expand Down Expand Up @@ -295,4 +293,5 @@ void message_cleanup(void) {
APP_MEM_FREE_AND_NULL((void **) &signMsgCtx->display_buffer);
}
APP_MEM_FREE_AND_NULL((void **) &signMsgCtx);
APP_MEM_FREE_AND_NULL((void **) &g_msg_hash_ctx);
}
Loading
Loading