Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f393272
Add swap feature
sportron-ledger Jul 2, 2026
2317d1d
Add swap tests
sportron-ledger Jul 3, 2026
b31b06e
fix: device getting stuck on the loading transaction screen when no f…
0xMM-L Jul 20, 2026
07b0ff1
Fix: fuzzing test
sportron-ledger Jul 23, 2026
2e1bf3e
Fix: clang format
sportron-ledger Jul 23, 2026
7d13984
Tests: add swap tests for tokens
sportron-ledger Jul 23, 2026
1c15ef1
Fix: build issue after rebasing with develop branch
sportron-ledger Aug 5, 2026
d4eccc1
Fix: ragger swap test for Fake token
sportron-ledger Aug 6, 2026
8931d93
CI: add swap tests
sportron-ledger Aug 6, 2026
0e858b5
Tests: add missing unit tests for swap
sportron-ledger Aug 6, 2026
42ceade
Bump version v1.2.1
sportron-ledger Aug 6, 2026
478bfde
Fix: Audit-2-3-5 : Overlapping root SIGN_TRANSACTION APDUs
sportron-ledger Aug 24, 2026
842eb83
Fix: Audit-6 : Fee prepared requests can be signed through nested-cal…
sportron-ledger Aug 27, 2026
87c4182
Fix: audit-4 : Premature execution signature release before fee final…
sportron-ledger Aug 27, 2026
6849a78
Bump version 1.3.0
sportron-ledger Aug 27, 2026
8538d32
Fix: V-063: View key remains in global UI context after confirmation …
sportron-ledger Aug 28, 2026
221fdc4
Fix: V-180: Precomputed Schnorr nonces retained in global r_list afte…
sportron-ledger Aug 28, 2026
54d72a9
Fix: V205: Malformed nested APDUs can keep a user approval alive inde…
sportron-ledger Aug 28, 2026
825132e
Fix: V-041: Key derivation and signing leave secret intermediates unc…
sportron-ledger Aug 28, 2026
07952a9
Fix: V-036: Private scalar material left on stack in group_scalar_mul…
sportron-ledger Aug 28, 2026
d1c5572
Fix: V-152: Out-of-bounds write in public plaintext hashing for struc…
sportron-ledger Aug 28, 2026
e23f73f
Fix: V-123: Sensitive signing intermediates persist in static buffers…
sportron-ledger Aug 28, 2026
2cc2581
Fix: V-186: Terminal swap return skips erasure of account private key…
sportron-ledger Aug 28, 2026
9cf6a9e
Fix: V-135: GET_TVK leaves derived account private keys in global con…
sportron-ledger Aug 28, 2026
22de46f
Fix: V-125: Poseidon global sponge retains secret-derived hash state …
sportron-ledger Aug 28, 2026
2cc15dd
Fix: send response fix
sportron-ledger Aug 28, 2026
0407b64
Fix: V-036: improve fix
sportron-ledger Aug 28, 2026
e612be9
Fix: V-041: improve fix
sportron-ledger Aug 28, 2026
a4cf1c9
Fix: V-123: improve fix
sportron-ledger Aug 28, 2026
1cf92f2
Fix: V-118: Signing state advances after failed transaction signature…
sportron-ledger Aug 28, 2026
4d314e4
Fix: V-117: Fee prepared requests can be signed through nested-call A…
sportron-ledger Aug 28, 2026
c2adae2
Fix: fee timeout blocking for touch devices
sportron-ledger Sep 1, 2026
1b28367
Bump version 1.3.1
sportron-ledger Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .github/workflows/build_and_functional_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ jobs:
download_app_binaries_artifact: "app_aleo_binaries"
regenerate_snapshots: ${{ github.event_name == 'workflow_dispatch' && inputs.golden_run == 'Open a PR' }}
test_dir: "tests/standalone"

tests_swap:
name: Run swap tests using the reusable workflow
needs: build_application
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_swap_tests.yml@v1
with:
download_app_binaries_artifact: "app_aleo_binaries"
regenerate_snapshots: ${{ github.event_name == 'workflow_dispatch' && inputs.golden_run == 'Open a PR' }}
1 change: 1 addition & 0 deletions APP_SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The following standard Status Words are returned for all APDUs.

| SW | SW name | Description |
| --- | --- | --- |
| 0x6400 | SWO_EXECUTION_ERROR | Execution error during APDU process |
| 0x6985 | SWO_CONDITIONS_NOT_SATISFIED | The app is not in the right state to execute the cmd |
| 0x69f0 | SWO_PERMISSION_DENIED | Rejected by user |
| 0x6a80 | SWO_INCORRECT_DATA | Given data is incorrect |
Expand Down
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,70 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.1] - 2026-09-01

### Fixed

- Fee timeout blocking for touch devices

## [1.3.0] - 2026-08-28

### Fixed

- V-063: View key remains in global UI context after confirmation or rejection
- V-180: Precomputed Schnorr nonces retained in global `r_list` after use
- V-205: Malformed nested APDUs can keep a user approval alive indefinitely
- V-041: Key derivation and signing leave secret intermediates uncleared on stack
- V-036: Private scalar material left on stack in `group_scalar_multiply`
- V-152: Out-of-bounds write in public plaintext hashing for struct/array inputs
- V-123: Sensitive signing intermediates persist in static buffers after successful operations
- V-186: Terminal swap return skips erasure of account private keys and signing nonces
- V-135: `GET_TVK` leaves derived account private keys in global context after `r_list` expiry
- V-125: Poseidon global sponge retains secret-derived hash state across APDU calls
- V-118: Signing state advances after failed transaction signature in review callback
- V-117: Fee prepared requests can be signed through nested-call APDU without fee-limit validation
- Audit-2-3-5: Overlapping root `SIGN_TRANSACTION` APDUs
- Audit-6: Fee prepared requests could be signed through a nested-call APDU; root tx processing is now blocked when not in the right state, and a 15s delay is enforced before the next nested call
- Audit-4: Premature execution signature release before fee finalization
- Ruff format issues

## [1.2.1] - 2026-08-06

### Added

- Swap feature support (Exchange app integration)
- Swap unit tests and Ragger tests, including tests for token swaps

### Fixed

- Device getting stuck on the loading transaction screen when no fees are present
- Fuzzing test issue
- Clang format issues
- Build issue after rebasing with `develop` branch
- Ragger swap test for the Fake token
- Ragger tests vanished during merge

### Changed

- CI: added swap tests

## [1.2.0] - 2026-08-04

### Fixed

- Audit-5: Allow pre-generated TVK only for private batch transfers
- Audit-8: Internal state not cleared before the end of the stackframe
- Audit-9: Missing error handling
- Audit-10: `hash_input` static variable not zeroed immediately after use
- Audit-11: Local variables not cleared on successful return path
- Audit-12: Repeated error processing in `sign_root_tx`
- Ruff linting issues
- Minor fixes

### Changed

- Migrate Python linting/formatting tooling to ruff

## [1.1.1] - 2026-06-26

### Added
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ APPNAME = "Aleo"

# Application version
APPVERSION_M = 1
APPVERSION_N = 2
APPVERSION_P = 0
APPVERSION_N = 3
APPVERSION_P = 1
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"

# Application source files
Expand Down Expand Up @@ -120,9 +120,9 @@ ifeq ($(APPNAME), "Aleo")
# - ENABLE_TESTING_SWAP: will lead to the enabling of the swap related C code of the standard_app
# ONLY works on Speculos, not on device
# Testing only SWAP flag
# ENABLE_TESTING_SWAP = 1
#ENABLE_TESTING_SWAP = 1
# Production enabled SWAP flag
# ENABLE_SWAP = 1
ENABLE_SWAP = 1
endif
# --8<-- [end:variables]

Expand Down
3 changes: 3 additions & 0 deletions build_dry_run_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
make: Entering directory '/app'
make: 'build/flex/obj/app/src/app_main.o' is up to date.
make: Leaving directory '/app'
1 change: 1 addition & 0 deletions fuzzing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ target_include_directories(
${APP_SRC}/account
${APP_SRC}/crypto
${APP_SRC}/db
${APP_SRC}/format
${APP_SRC}/handler
${APP_SRC}/helper
${APP_SRC}/transaction
Expand Down
67 changes: 46 additions & 21 deletions src/account/account.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@

#include "account.h"

#define HASH_INPUT_MAX_LENGTH (8)
/*#define HASH_INPUT_MAX_LENGTH (8)

static field_t hash_input[HASH_INPUT_MAX_LENGTH];
static char text_buffer[32];
static field_t hash_input[HASH_INPUT_MAX_LENGTH];*/
Comment on lines +35 to +37
static char text_buffer[32];

const field_t ACCOUNT_SK_SIG_DOMAIN = {
.big.u64 = {0xc9a73b0068afb54b, 0x95d2050edfd00d2d, 0x30b27b31e4cc8dc3, 0x127ef5e8bbf7590e}
Expand Down Expand Up @@ -66,6 +66,7 @@

static int get_seed(const uint32_t *path, uint8_t path_len, field_t *seed)
{
int status = -1;
uint8_t seed_bn[BN_LENGTH];
bigint_256_t seed_big_int;
bolos_err_t error = sys_hdkey_derive(HDKEY_DERIVE_MODE_BLS12377_ALEO,
Expand All @@ -79,21 +80,29 @@
NULL,
0);
if (error != SWO_OK) {
return -1;
status = -1;
goto end;
}
bn_print(seed_bn);

bn_reverse(seed_bn);
bn_to_big_int(seed_bn, &seed_big_int);
field_from_big_int(seed, &seed_big_int);

return 0;
status = 0;

end:
explicit_bzero(seed_bn, sizeof(seed_bn));
explicit_bzero(&seed_big_int, sizeof(seed_big_int));

return status;
}

static int private_key_from_seed(const field_t *seed, scalar_t *sk_sig, scalar_t *r_sig)
{
_Static_assert(HASH_INPUT_MAX_LENGTH >= 4, "hash_input size won't fit");
int status = -1;
//_Static_assert(HASH_INPUT_MAX_LENGTH >= 4, "hash_input size won't fit");
int status = -1;
field_t hash_input[4];

// Compute sk_sig
memset(hash_input, 0, sizeof(hash_input));
Expand Down Expand Up @@ -142,23 +151,23 @@
group_println(&compute_key->pr_sig);

end:
explicit_bzero(hash_input, sizeof(hash_input));
return status;
}

static int view_key_from_private_and_compute_key(const private_key_t *private_key,
compute_key_t *compute_key,
scalar_t *view_key)
{
_Static_assert(HASH_INPUT_MAX_LENGTH >= 6, "hash_input size won't fit");
int status = -1;
//_Static_assert(HASH_INPUT_MAX_LENGTH >= 6, "hash_input size won't fit");
int status = -1;
field_t hash_input[6];

memset(hash_input, 0, sizeof(hash_input));
memcpy(&hash_input[4], &compute_key->pk_sig.x, sizeof(field_t));
memcpy(&hash_input[5], &compute_key->pr_sig.x, sizeof(field_t));
status = hash_to_scalar_psd4(hash_input, 4 + 2, &compute_key->sk_prf);
if (status < 0) {
return -1;
goto end;
}
PRINTF("sk_prf : ");
scalar_println(&compute_key->sk_prf);
Expand All @@ -169,6 +178,8 @@
PRINTF("view_key : ");
scalar_println(view_key);

end:
explicit_bzero(hash_input, sizeof(hash_input));
return status;
}

Expand All @@ -186,9 +197,11 @@

static int graph_key_from_view_key(const scalar_t *view_key, field_t *graph_key)
{
_Static_assert(HASH_INPUT_MAX_LENGTH >= 7, "hash_input size won't fit");
//_Static_assert(HASH_INPUT_MAX_LENGTH >= 7, "hash_input size won't fit");
int status = -1;
field_t hash_input[7];
field_t f_view_key;

scalar_to_field(view_key, &f_view_key);

memset(hash_input, 0, sizeof(hash_input));
Expand All @@ -204,6 +217,8 @@

end:
explicit_bzero(hash_input, sizeof(hash_input));
explicit_bzero(&f_view_key, sizeof(f_view_key));

return status;
}

Expand Down Expand Up @@ -266,8 +281,9 @@
PRINTF("%s\n", address);

end:
explicit_bzero(hash_input, sizeof(hash_input));
explicit_bzero(&account, sizeof(account_t));
explicit_bzero(&address_big_int, sizeof(address_big_int));
explicit_bzero(address_bn, sizeof(address_bn));

return status;
}
Expand Down Expand Up @@ -320,8 +336,10 @@
PRINTF("%s\n", viewkey);

end:
explicit_bzero(hash_input, sizeof(hash_input));
explicit_bzero(&account, sizeof(account_t));
explicit_bzero(&view_key_big_int, sizeof(view_key_big_int));
explicit_bzero(view_key_bn, sizeof(view_key_bn));
explicit_bzero(base_58_input, sizeof(base_58_input));

return status;
}
Expand Down Expand Up @@ -381,12 +399,14 @@
goto error;
}

explicit_bzero(hash_input, sizeof(hash_input));
return 0;
goto end;

error:
explicit_bzero(hash_input, sizeof(hash_input));
explicit_bzero(account, sizeof(account_t));

end:
explicit_bzero(&address_big_int, sizeof(address_big_int));
explicit_bzero(address_bn, sizeof(address_bn));
return status;
}

Expand All @@ -399,6 +419,7 @@
{
int status = -1;
field_t nonce;
field_t hash_input[8];
scalar_t *r = NULL;

LEDGER_ASSERT(account != NULL, "NULL account");
Expand All @@ -418,7 +439,7 @@
r = &G_context.r_list.array[index];

// Compute a `r0` as `hash_to_scalar_psd4(domain || sk_sig || nonce)`
_Static_assert(HASH_INPUT_MAX_LENGTH >= 7, "hash_input size won't fit");
//_Static_assert(HASH_INPUT_MAX_LENGTH >= 7, "hash_input size won't fit");
memset(hash_input, 0, sizeof(hash_input));
memcpy(&hash_input[4], &LEDGER_APP_ALEO_DOMAIN, sizeof(field_t));
scalar_to_field(&account->private_key.sk_sig, &hash_input[5]);
Expand Down Expand Up @@ -450,7 +471,7 @@
}

// Compute a `rx` as `hash_to_scalar_psd4(domain || sk_sig || nonce || index)`
_Static_assert(HASH_INPUT_MAX_LENGTH >= 8, "hash_input size won't fit");
//_Static_assert(HASH_INPUT_MAX_LENGTH >= 8, "hash_input size won't fit");
memset(hash_input, 0, sizeof(hash_input));
memcpy(&hash_input[4], &LEDGER_APP_ALEO_DOMAIN, sizeof(field_t));
scalar_to_field(&account->private_key.sk_sig, &hash_input[5]);
Expand All @@ -471,14 +492,15 @@
scalar_println(r);

end:
explicit_bzero(&nonce, sizeof(nonce));
explicit_bzero(hash_input, sizeof(hash_input));
if (status < 0) {
r_list_erase();
}
return status;
}

int r_list_get(uint8_t index, scalar_t *r)
int r_list_get(uint8_t index, scalar_t *r, bool erase)
{
int status = -1;

Expand All @@ -494,6 +516,9 @@
}
status = 0;
memcpy(r, &G_context.r_list.array[index], sizeof(scalar_t));
if (erase) {
explicit_bzero(&G_context.r_list.array[index], sizeof(scalar_t));
}

end:
return status;
Expand All @@ -508,7 +533,7 @@
LEDGER_ASSERT(account != NULL, "NULL account");
LEDGER_ASSERT(tvk != NULL, "NULL tvk");

if ((status = r_list_get(index, &r)) < 0) {
if ((status = r_list_get(index, &r, false)) < 0) {
goto end;
}
PRINTF("R%d : ", index);
Expand Down
2 changes: 1 addition & 1 deletion src/account/account.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ int account_generate_keys(const uint32_t *path, uint8_t path_len, account_t *acc
void account_erase(account_t *account);

int r_list_set(account_t *account, uint8_t index);
int r_list_get(uint8_t index, scalar_t *r);
int r_list_get(uint8_t index, scalar_t *r, bool erase);
int r_list_get_tvk(account_t *account, uint8_t index, field_t *tvk);
void r_list_erase(void);
Loading
Loading