Skip to content

Fix cerberus low#1029

Merged
cedelavergne-ledger merged 7 commits into
developfrom
cev/fix_cerberus_low
May 12, 2026
Merged

Fix cerberus low#1029
cedelavergne-ledger merged 7 commits into
developfrom
cev/fix_cerberus_low

Conversation

@cedelavergne-ledger
Copy link
Copy Markdown
Contributor

Description

https://ledgerhq.atlassian.net/browse/B2CA-2627

Changes include

  • Bugfix (non-breaking change that solves an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (change that is not backwards-compatible and/or changes current functionality)
  • Tests
  • Documentation
  • Other (for changes that might not fit in any category)

@cedelavergne-ledger cedelavergne-ledger requested a review from a team as a code owner May 12, 2026 08:28
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.83%. Comparing base (beca278) to head (89db014).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1029   +/-   ##
========================================
  Coverage    59.83%   59.83%           
========================================
  Files           26       26           
  Lines         2612     2612           
  Branches       334      334           
========================================
  Hits          1563     1563           
  Misses        1045     1045           
  Partials         4        4           
Flag Coverage Δ
unittests 59.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

elf sizes
source = source branch cev/fix_cerberus_low
target = target branch develop

Device .text source .text target .text delta .bss source .bss target .bss delta max stack size source max stack size target max stack size delta
apex_p 161945 161433 512 21810 21810 0 19144 19144 0
flex 182093 182093 0 21810 21810 0 15048 15048 0
stax 182014 182014 0 21810 21810 0 15048 15048 0
nanos2 115811 115811 0 20508 20508 0 20448 20448 0
nanox 115539 115283 256 20536 20536 0 8192 8192 0

Stack consumption summary (clone_app_stack_consumption)

⚠️ This summary is for informative purpose only. It may not give the application actual worst case, for example if the test coverage is low.

Device Worst case (bytes) Remaining stack (bytes) Test
apex_p 1753 38599 test_clone.py::test_clone_thundercore[apex_p]
flex 1801 34455 test_clone.py::test_clone_thundercore[flex]
nanosp 1705 38647 test_clone.py::test_clone_thundercore[nanosp]
nanox 1697 6495 test_clone.py::test_clone_thundercore[nanox]
stax 1801 34455 test_clone.py::test_clone_thundercore[stax]

Full details

Stack consumption summary

⚠️ This summary is for informative purpose only. It may not give the application actual worst case, for example if the test coverage is low.

Device Worst case (bytes) Remaining stack (bytes) Test
apex_p 3193 15951 test_eip712.py::test_eip712_batch[apex_p]
flex 3193 11855 test_eip712.py::test_eip712_batch[flex]
nanosp 3193 17255 test_eip712.py::test_eip712_batch[nanosp]
nanox 3193 4999 test_eip712.py::test_eip712_batch[nanox]
stax 3193 11855 test_eip712.py::test_eip712_batch[stax]

Full details

ui_display_privacy_shared_secret() told the user they were providing a
"public secret key". The corresponding command path is actually an
X25519 derivation that returns a 32-byte shared secret computed from
the device-held private key and an external public key, not a public
value. On a hardware wallet, approval text is the last security
boundary; inaccurate wording can materially change whether a user
consents to secret disclosure.

Rename the confirmation header to "Provide derived shared secret" so
the title accurately describes the secret material being released.
get_eth2_public_key() derived an EIP-2333 BLS12-381 private scalar
into the stack buffer privateKeyData but never erased it before
returning. The neighboring locals (tmp, privateKey) were explicitly
scrubbed in the end: cleanup block, which indicates the omission was
accidental.

On embedded targets, residual stack contents can persist across calls
and become recoverable through later memory-disclosure bugs, crash
dumps, or forensic extraction. Exposure of validator private keys can
lead to unauthorized signing or loss of staking/account control.

Add explicit_bzero(privateKeyData, sizeof(privateKeyData)) to the
existing cleanup block.
process_chain_id() accepted chainId fields up to INT256_LENGTH (32
bytes) for EIP-2930 / EIP-1559 / EIP-7702 transactions, but downstream
get_tx_chain_id() converts the stored value to uint64_t via
u64_from_BE() which silently consumes only the first 8 bytes. The
signature hash still covers the full original encoding, so a malicious
host could craft a transaction whose signature applies to one chain
while the device displays / validates only the truncated 64-bit prefix.

Restrict the field length validation to sizeof(uint64_t). Any chain ID
that does not fit in a uint64_t is now rejected at the parser level
rather than being truncated, keeping the UI / network selection / chain
consistency checks aligned with what gets signed.
The app exposes a "Always display the transaction or message hash"
setting and the EIP-191 signing path computes the message hash before
launching the review. However, the NBGL personal-message review never
consulted N_storage.displayHash and never added the hash to the review
pairs. Users who explicitly enabled hash display did not actually see
the hash for personal-message signing, reducing their ability to
verify the exact bytes being signed when rendering is ambiguous
(whitespace, encoding differences, long content, hex fallback).

Mirror the transaction-hash flow already used in ui_approve_tx: when
N_storage.displayHash is set, format tmpCtx.messageSigningContext.hash
into strings.common.tx_hash and append it as a second review pair
labelled "Message hash". The setting toggle now applies consistently
to EIP-191 messages, EIP-712 typed data, and transactions.
handle_perform_privacy_operation() stores the derived X25519 shared
secret in tmpCtx.publicKeyContext.publicKey and, on the confirmation
path, copies a hex view of the secret into strings.common.fullAmount
for UI rendering. Neither global was scrubbed before returning:
set_result_perform_privacy_operation() only copied the secret into the
APDU reply buffer, successful synchronous APDUs were not followed by
reset_app_context(), and reset_app_context() itself did not clear the
strings buffer.

As a result, the last derived shared secret could remain resident in
RAM across later APDU exchanges, where a memory-disclosure bug, crash
dump, fault injection, or forensic extraction could recover it.

Two changes, kept together so the residue is closed end-to-end:

  * logic_perform_privacy_operation.c: scrub tmpCtx.publicKeyContext
    immediately after the secret has been copied to G_io_tx_buffer.
  * main.c: zero the whole strings struct in reset_app_context() so
    any UI-formatted view of secrets (and other transient strings)
    does not persist across resets.
decode_function_selector() automatically queried 4byte.directory for
any selector that was not present in the local cache. Operators
running the decoder on captured APDU traces had no way to keep the
analysis offline, and the function selectors of internal contracts
or private integrations could leak to a third-party host without an
explicit prompt.

Add an --online-selectors flag (default off). When set, main() flips
the module-level ALLOW_ONLINE_SELECTOR_LOOKUP gate and emits a clear
warning before any external request. Without the flag the decoder
returns "Unknown (0x...)" for cache misses and never reaches the
network (CWE-201).
InputData.process_data() reset current_path on entry but left
filtering_paths, filtering_tokens, filtering_calldatas and sig_ctx
populated whenever the `filters` argument was omitted. As a result, a
prior filtered signing flow contaminated later supposedly-unfiltered
flows in the same Python process: the helper would replay leftover
filter descriptors as extra APDUs and corrupt downstream snapshot
comparisons or assertions.

Clear every piece of module-level state at the start of each call,
regardless of whether filters are provided. This makes the helper
re-entrant within a single test process so cross-test state leakage
no longer hides UI/signing regressions (CWE-664).
@cedelavergne-ledger cedelavergne-ledger merged commit e94a676 into develop May 12, 2026
298 of 300 checks passed
@cedelavergne-ledger cedelavergne-ledger deleted the cev/fix_cerberus_low branch May 12, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants