Skip to content

Fix cerberus Medium#1028

Merged
cedelavergne-ledger merged 11 commits into
developfrom
cev/fix_cerberus_medium
May 12, 2026
Merged

Fix cerberus Medium#1028
cedelavergne-ledger merged 11 commits into
developfrom
cev/fix_cerberus_medium

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)

Breaking changes

Please complete this section if any breaking changes have been made, otherwise delete it.

Additional comments

Please post additional comments in this section if you have them, otherwise delete it.

@cedelavergne-ledger cedelavergne-ledger requested a review from a team as a code owner May 11, 2026 16:54
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 11, 2026

Codecov Report

❌ Patch coverage is 99.03382% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.83%. Comparing base (e354f38) to head (66b9844).

Files with missing lines Patch % Lines
src/features/generic_tx_parser/gtp_param_raw.c 95.34% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1028      +/-   ##
===========================================
+ Coverage    56.04%   59.83%   +3.79%     
===========================================
  Files           26       26              
  Lines         2414     2612     +198     
  Branches       313      334      +21     
===========================================
+ Hits          1353     1563     +210     
- Misses        1005     1045      +40     
+ Partials        56        4      -52     
Flag Coverage Δ
unittests 59.83% <99.03%> (+3.79%) ⬆️

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 11, 2026

elf sizes
source = source branch cev/fix_cerberus_medium
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 161433 161433 0 21810 21810 0 19144 19144 0
nanos2 115811 115299 512 20508 20508 0 20448 20448 0
stax 182014 181502 512 21810 21810 0 15048 15048 0
nanox 115283 115027 256 20536 20536 0 8192 8192 0
flex 182093 181581 512 21810 21810 0 15048 15048 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

@cedelavergne-ledger cedelavergne-ledger force-pushed the cev/fix_cerberus_medium branch 2 times, most recently from 75784b3 to 191e4a5 Compare May 12, 2026 06:49
cedelavergne-ledger and others added 11 commits May 12, 2026 15:32
The packaged Python client's TxAuth7702 constructor accepted
chain_id=None and silently coerced it to 0. The TLV with chain_id=0
crossed the host->device boundary and was interpreted by the firmware
as CHAIN_ID_ALL, producing an authorization signature valid on every
chain. A bridge or dApp integration carrying chain_id through Optional
types could end up requesting a far broader signing scope than intended
without any client-side warning.

Tighten the type contract: chain_id is now a required int, with no
Optional in the signature and no silent coercion of None. Callers that
genuinely want an all-chains authorization must pass the explicit
TxAuth7702.CHAIN_ID_ALL sentinel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
handle_first_sign_chunk() allocates the global keccak context
g_tx_hash_ctx via APP_MEM_CALLOC and only the success path
(finalize_parsing_helper) and a subset of post-init parsing errors
(handle_sign:180) release it. Other early-error returns inside
handle_first_sign_chunk (init_tx failure, invalid data offset,
unsupported tx type, cx_hash_no_throw failure) propagate the SW
directly to handle_sign and out to main.c without freeing the
allocation. Likewise, reset_app_context() never released the context,
so app_quit() and the implicit reset at the top of
handle_first_sign_chunk left previous-session allocations dangling. A
host repeatedly triggering aborted INS_SIGN first-chunk APDUs could
exhaust the device's app heap.

Add a NULL-checked APP_MEM_FREE_AND_NULL of g_tx_hash_ctx to
reset_app_context(). Errors that bubble out of handle_sign reach
main.c with reset=true, which now centrally releases the context. The
existing late-error free in handle_sign is kept as defense in depth;
the NULL check makes the double path safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…play

The legacy signing path accepted transactions that omit the EIP-155
chain_id encoding (vLength == 0) and produced a signature with the
legacy v base 27/28. Such signatures are not domain-separated by chain
and can be replayed on any EVM-compatible chain that still accepts
unprotected legacy transactions. The existing chain-consistency check
in finalize_parsing_helper only enforced strict matching for non-mainnet
app configurations, so on Ethereum mainnet the replayable mode was
never rejected.

Reject any LEGACY transaction whose RLP V field is empty, regardless of
app configuration. EIP-155 has been mandatory for safe replay protection
since 2016, and no current ragger tests rely on the unprotected form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…fields

format_param_token() declared `ticker` and `token_info` outside the
collection loop. When an iteration matched the native-currency
branch, only `ticker` was overwritten while `token_info` kept the
value left over from a previous ERC-20 match. The stale `token_info`
was then handed to add_to_field_table(), so a generic-parser descriptor
that mixes native and unknown ERC-20 addresses could mislabel later
entries with the previous token's metadata.

Move `ticker` and `token_info` into the per-iteration scope so they
always start at NULL, and force the matching branch to repopulate them
from scratch (the native branch keeps `token_info == NULL` intentionally).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ields

format_param_raw() accepted MUST_BE and IF_NOT_IN visibility constraints
at the field level but format_int(), format_bool() and format_string()
never called apply_visibility_constraint(). The hide/reject rule encoded
in a signed generic-parser descriptor was therefore a no-op for any
TF_INT, TF_BOOL or TF_STRING parameter, letting attacker-controlled
calldata be approved with misleading on-device output (e.g. a signed
delta the descriptor was meant to constrain, or a boolean toggle hidden
behind an IF_NOT_IN list).

Reshape the three formatters to mirror the existing UINT/ADDRESS/BYTES
flow: they now take the `s_field` and a `to_be_displayed` flag, format
the value first, then evaluate the constraints and call
apply_visibility_constraint(). Per-type comparisons:
  * INT  — canonical decimal-string equality (matches sign-extended
    encodings of varying byte widths).
  * BOOL — both sides normalized to 0/1 before comparison.
  * STRING — byte-level length + memcmp equality.

Also reset `to_be_displayed = true` at the start of every iteration so
that an IF_NOT_IN hit on one collection element no longer drags
subsequent elements out of the review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
INS_PERFORM_PRIVACY_OPERATION (0x18) accepted P1_NON_CONFIRM for both
the public encryption key (P2=0x00) and the shared secret (P2=0x01).
The shared secret is computed via X25519 from the device-derived
private key and an attacker-supplied Curve25519 public key, so it is
secret material that should never leave the device without the user
physically approving the action. The non-confirm path turned the device
into a silent ECDH oracle reachable by any host that could send APDUs.

Reject P2_SHARED_SECRET + P1_NON_CONFIRM with
SWO_CONDITIONS_NOT_SATISFIED. The P2_PUBLIC_ENCRYPTION_KEY path keeps
its existing non-confirm semantics — that operation only exposes a
public key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
handle_safe_tlv_payload() and handle_signer_tlv_payload() verify the
signed CHALLENGE field of their respective descriptors but never call
roll_challenge() afterwards. The global challenge issued by
INS_GET_CHALLENGE stayed valid until the next explicit GET_CHALLENGE,
so any previously captured signed Safe / Signer descriptor could be
replayed within the same app session and the device would display
stale signer / threshold information as freshly authorized.

Roll the challenge at the end of both TLV payload handlers, on both
success and failure, mirroring the pattern already used by the trusted
name and proxy info flows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
path_update() walks nested TYPE_CUSTOM fields and allocates a fresh
keccak hash context on every step via push_new_hash_depth(). When the
descent reached MAX_PATH_DEPTH, path_depth_list_push() returned false
but its return value was ignored, so the next iteration kept
descending and kept calling push_new_hash_depth(). For a self-referential
or cyclic schema (A { A a; } or A -> B -> A), this turned into an
unbounded allocation loop until the app's heap was exhausted, denying
service to typed-message signing.

Propagate the path_depth_list_push() failure: when the path stack is
full, abort the descent with SWO_INCORRECT_DATA. The depth cap bounds
the loop for any recursive schema while leaving legitimate nested
non-recursive types unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…matter

Cover the constraint paths added by the CWE-693 fix. The pre-existing
test_param_raw.c had no TF_INT coverage at all, and TF_BOOL / TF_STRING
were only exercised for the unconstrained 'always display' path.

Add:
  * TF_INT  - positive value, negative value (regression for CWE-451
    8/16-bit signed rendering), and the four constraint matrix entries
    (MUST_BE valid/invalid, IF_NOT_IN match/no_match).
  * TF_BOOL - four constraint matrix entries.
  * TF_STRING - four constraint matrix entries.

A new CREATE_INT_PARAM helper macro fills in the s_value type_size that
format_signed_int_be needs for sign extension.
Many serialize() methods declared '-> bytes' but returned a bytearray
they had built up incrementally. Python's mypy does not consider
bytearray to be a bytes, so each return-value was flagged. SigningPartner
from ragger.pki also accepts strictly bytes for sign(), but the codebase
was passing the bytearray buffers directly.

Convert at the API boundary: wrap returns with bytes(), wrap sign() args
with bytes(). Widen the local _serialize() helper's cdata parameter to
Union[bytes, bytearray] since it's an internal type and many callers pass
the bytearray buffer they're building. No behavioral change.
@cedelavergne-ledger cedelavergne-ledger force-pushed the cev/fix_cerberus_medium branch from 191e4a5 to 66b9844 Compare May 12, 2026 13:33
@cedelavergne-ledger cedelavergne-ledger merged commit beca278 into develop May 12, 2026
293 checks passed
@cedelavergne-ledger cedelavergne-ledger deleted the cev/fix_cerberus_medium branch May 12, 2026 13:58
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