Skip to content

Commit e94a676

Browse files
Merge pull request #1029 from LedgerHQ/cev/fix_cerberus_low
Fix cerberus low
2 parents beca278 + 89db014 commit e94a676

8 files changed

Lines changed: 113 additions & 8 deletions

File tree

client/src/ledger_app_clients/ethereum/eip712/InputData.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,23 @@ def process_data(aclient: EthAppClient,
595595
filters: Optional[dict] = None) -> None:
596596
global app_client
597597
global current_path
598-
598+
global filtering_paths
599+
global filtering_tokens
600+
global filtering_calldatas
601+
global sig_ctx
602+
603+
# Reset every piece of module-level state at the start of each call so
604+
# that a previous filtered run cannot contaminate the next one. The
605+
# previous behavior reset current_path but left filtering_paths,
606+
# filtering_tokens, filtering_calldatas and sig_ctx populated whenever
607+
# `filters` was omitted, leading to silent cross-test state leakage
608+
# (CWE-664).
599609
current_path = []
610+
filtering_paths = {}
611+
filtering_tokens = []
612+
filtering_calldatas = []
613+
sig_ctx = {}
614+
600615
# deepcopy because this function modifies the dict
601616
data_json = copy.deepcopy(data_json)
602617
app_client = aclient

src/features/get_eth2_public_key/cmd_get_eth2_public_key.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ uint32_t get_eth2_public_key(uint32_t *bip32Path, uint8_t bip32PathLength, uint8
5454
publicKey.W[1] |= 0x80 | yFlag;
5555
memmove(out, publicKey.W + 1, BLS12381_G1_COMPRESSED_PUBKEY_LENGTH);
5656
end:
57+
// privateKeyData holds the EIP-2333-derived BLS12-381 private scalar (32
58+
// bytes of secret material). Residual stack content can be recovered
59+
// through later memory-disclosure bugs, crash dumps, or forensic
60+
// extraction, so wipe it before returning (CWE-226).
61+
explicit_bzero(privateKeyData, sizeof(privateKeyData));
5762
explicit_bzero(tmp, BLS12381_G1_UNCOMPRESSED_PUBKEY_LENGTH);
5863
explicit_bzero((void *) &privateKey, sizeof(cx_ecfp_256_extended_private_key_t));
5964
return error;

src/features/perform_privacy_operation/logic_perform_privacy_operation.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ uint32_t set_result_perform_privacy_operation() {
44
for (uint8_t i = 0; i < INT256_LENGTH; i++) {
55
G_io_tx_buffer[i] = tmpCtx.publicKeyContext.publicKey.W[INT256_LENGTH - i];
66
}
7+
// Scrub the source as soon as the secret has been copied to the reply
8+
// buffer. For the P2_SHARED_SECRET path this is X25519 secret material;
9+
// we don't want it lingering in tmpCtx until the next reset (CWE-312).
10+
explicit_bzero(&tmpCtx.publicKeyContext, sizeof(tmpCtx.publicKeyContext));
711
return INT256_LENGTH;
812
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
#include "apdu_constants.h"
2+
#include "shared_context.h"
23
#include "ui_callbacks.h"
34
#include "feature_perform_privacy_operation.h"
45

6+
// The shared-secret review path renders the X25519 derived secret as hex into
7+
// strings.common.fullAmount, and the corresponding device address into
8+
// strings.common.toAddress. Scrub them on every exit so the secret view does
9+
// not linger in RAM (CWE-312). Surgical instead of a global strings reset:
10+
// other flows reuse the same union for non-sensitive review state.
11+
static void scrub_privacy_strings(void) {
12+
explicit_bzero(strings.common.fullAmount, sizeof(strings.common.fullAmount));
13+
explicit_bzero(strings.common.toAddress, sizeof(strings.common.toAddress));
14+
}
15+
516
unsigned int io_seproxyhal_touch_privacy_ok(void) {
617
uint32_t tx = set_result_perform_privacy_operation();
18+
scrub_privacy_strings();
719
return io_seproxyhal_send_status(SWO_SUCCESS, tx, true, true);
820
}
921

1022
unsigned int io_seproxyhal_touch_privacy_cancel(void) {
23+
scrub_privacy_strings();
1124
return io_seproxyhal_send_status(SWO_CONDITIONS_NOT_SATISFIED, 0, true, true);
1225
}

src/features/sign_tx/eth_ustream.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,18 @@ static bool process_auth_list(txContext_t *context) {
168168
}
169169

170170
static bool process_chain_id(txContext_t *context) {
171-
if (check_fields(context, "RLP_CHAINID", INT256_LENGTH) == false) {
171+
// Reject chain IDs that would not fit in a uint64_t. Downstream
172+
// get_tx_chain_id() converts the stored bytes through u64_from_BE(),
173+
// silently truncating anything beyond 8 bytes — so a 32-byte field
174+
// could let the signature cover one chain while the UI / consistency
175+
// checks operate on the truncated 64-bit prefix (CWE-197).
176+
//
177+
// This is a hardening, not a regression: EIP-2294 recommends bounding
178+
// chain_id to 2^53 for interoperability, and no production EVM chain
179+
// today uses a value beyond uint64_t. Pre-fix, anything beyond 8 bytes
180+
// was still hashed into the signature but never matched on display, so
181+
// there is no legitimate flow this rejection breaks.
182+
if (check_fields(context, "RLP_CHAINID", sizeof(uint64_t)) == false) {
172183
return false;
173184
}
174185

src/nbgl/ui_display_privacy.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,9 @@ void ui_display_privacy_public_key(void) {
3939
}
4040

4141
void ui_display_privacy_shared_secret(void) {
42-
buildFirstPage("Provide public\nsecret key");
42+
// The value released here is the X25519 shared secret derived from the
43+
// device-held private key and the host-supplied peer public key — it is
44+
// NOT a public value. Wording must make that clear so the user does not
45+
// approve secret disclosure thinking it is a public-key export.
46+
buildFirstPage("Provide derived\nshared secret");
4347
}

src/nbgl/ui_sign_message.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,27 @@ static void ui_191_finish_cb(bool confirm) {
2121
}
2222

2323
void ui_191_start(const char *message) {
24-
// Initialize the buffers
25-
if (!ui_pairs_init(1)) {
24+
// Honor the "Always display the transaction or message hash" setting for
25+
// EIP-191 personal messages so that the user can verify the exact bytes
26+
// being signed even when rendering is ambiguous (whitespace, encoding,
27+
// long content, hex fallback). The tx hash flow does this already; the
28+
// message review used to silently ignore the toggle (CWE-451).
29+
//
30+
// Format the hash first and only count it as a pair if formatting
31+
// succeeded — otherwise we would advertise a 2-pair list with an
32+
// uninitialized g_pairs[1] and the device would render garbage.
33+
bool show_hash = false;
34+
if (N_storage.displayHash) {
35+
strlcpy(strings.common.tx_hash, "0x", 3);
36+
if (bytes_to_lowercase_hex(strings.common.tx_hash + 2,
37+
sizeof(strings.common.tx_hash) - 2,
38+
tmpCtx.messageSigningContext.hash,
39+
INT256_LENGTH) >= 0) {
40+
show_hash = true;
41+
}
42+
}
43+
44+
if (!ui_pairs_init(show_hash ? 2 : 1)) {
2645
// Initialization failed, cleanup and return
2746
return;
2847
}
@@ -44,8 +63,13 @@ void ui_191_start(const char *message) {
4463
ui_tx_simulation_finish_str());
4564

4665
g_pairsList->wrapping = true;
47-
g_pairs->item = "Message";
48-
g_pairs->value = message;
66+
g_pairs[0].item = "Message";
67+
g_pairs[0].value = message;
68+
69+
if (show_hash) {
70+
g_pairs[1].item = "Message hash";
71+
g_pairs[1].value = strings.common.tx_hash;
72+
}
4973

5074
#ifndef FUZZ
5175
nbgl_useCaseAdvancedReview(TYPE_MESSAGE,

tools/decode_apdu.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626

2727
# Local selector cache at module level
2828
LOCAL_SELECTORS = {}
29+
# Whether to fall back to 4byte.directory for selectors absent from the local
30+
# cache. Opt-in only — set from --online-selectors in main() so that running
31+
# the decoder over a captured APDU trace does not silently disclose its
32+
# function selectors to a third-party host (CWE-201).
33+
ALLOW_ONLINE_SELECTOR_LOOKUP = False
2934
CACHE_FILE = Path(__file__).parent / "function_selectors.json"
3035

3136
logger = logging.getLogger(__name__)
@@ -49,6 +54,15 @@ def init_parser() -> argparse.ArgumentParser:
4954
parser = argparse.ArgumentParser(description="Decode APDU replay file to extract transaction details.")
5055
parser.add_argument("--input", "-i", required=True, help="Input apdu replay file.")
5156
parser.add_argument("--verbose", "-v", action='store_true', help="Verbose mode")
57+
parser.add_argument(
58+
"--online-selectors",
59+
action="store_true",
60+
help=(
61+
"Allow unknown function selectors to be looked up online against "
62+
"4byte.directory. By default the decoder works offline and leaks "
63+
"no replay data over the network (CWE-201)."
64+
),
65+
)
5266
return parser
5367

5468

@@ -123,7 +137,12 @@ def decode_function_selector(selector: str) -> str:
123137
logger.debug(f"Found selector {selector} in local cache")
124138
return LOCAL_SELECTORS[selector]
125139

126-
# 2. Try online API as fallback
140+
# 2. Try online API as fallback — only when the user explicitly asked for
141+
# it. Without the opt-in we never leak the selector to 4byte.directory.
142+
if not ALLOW_ONLINE_SELECTOR_LOOKUP:
143+
logger.debug(f"Selector {selector} not in cache; online lookup disabled")
144+
return f"Unknown (0x{selector})"
145+
127146
logger.debug(f"Selector {selector} not in cache, querying API...")
128147
try:
129148
url = f"https://www.4byte.directory/api/v1/signatures/?hex_signature=0x{selector}"
@@ -1024,11 +1043,21 @@ def parse_apdu_line(line: str) -> Optional[bytes]:
10241043
# Main entry
10251044
# ===============================================================================
10261045
def main() -> None:
1046+
global ALLOW_ONLINE_SELECTOR_LOOKUP
1047+
10271048
parser = init_parser()
10281049
args = parser.parse_args()
10291050

10301051
set_logging(args.verbose)
10311052

1053+
if args.online_selectors:
1054+
ALLOW_ONLINE_SELECTOR_LOOKUP = True
1055+
logger.warning(
1056+
"Online selector lookup enabled: unknown function selectors from "
1057+
"the replay will be sent to https://www.4byte.directory/. Do not "
1058+
"use this flag on sensitive traces."
1059+
)
1060+
10321061
# Load selector cache at startup
10331062
load_selector_cache()
10341063
logger.debug(f"Loaded {len(LOCAL_SELECTORS)} function selectors from cache")

0 commit comments

Comments
 (0)