What changed, and why it matters
This commit adds a user-controlled setting to the Ledger Bitcoin app that blocks non-standard Bitcoin transaction signing modes by default. Previously, these modes were allowed with only a warning. Now the app rejects them unless the user explicitly turns on 'Allow non-standard sighash' in settings and confirms a warning. This is a security-hardening change, not a vulnerability fix in the traditional sense, but it reduces the risk of users accidentally signing transactions that protect fewer funds than they expect.
No immediate action required; this is a hardening commit. Users should review whether they want the new default behavior (non-standard sighash types blocked). Developers should verify that the classifier correctly handles all Taproot/segwit edge cases and that the setting cannot be toggled without user confirmation on the device.
Security signals we found
Adds default-deny gate for non-standard Bitcoin sighash types
Introduces persistent NVRAM setting for security-relevant behavior
Adds explicit warning confirmation dialog before enabling risky setting
Returns new dedicated error code when non-standard sighash is disabled
Refactors sighash classification into a dedicated module with documented security semantics
Evidence from the diff
The commit introduces a new NVRAM-backed application setting (allow_nondefault_sighash), a classifier for sighash types (SAFE vs NON_SAFE vs UNSUPPORTED), and UI controls to toggle the setting. During PSBT signing preprocessing, inputs with non-standard sighash types (SIGHASH_NONE, SIGHASH_SINGLE, and ANYONECANPAY combinations) are now rejected with SW_SECURITY_STATUS_NOT_SATISFIED / EC_SIGN_PSBT_NONDEFAULT_SIGHASH_NOT_ALLOWED unless the user has enabled the setting. The change refactors existing sighash logic into classify_sighash() and gates the previously warning-only path behind the new setting.
Changed components
src/app_settings.csrc/app_settings.hsrc/common/sighash.hsrc/error_codes.hsrc/handler/sign_psbt/preprocess_inputs.csrc/ui/menu_nbgl.cInspect captured patch +220 / −11
diff --git a/src/app_settings.c b/src/app_settings.c
new file mode 100644
index 0000000..4f069a9
--- /dev/null
+++ b/src/app_settings.c
@@ -0,0 +1,9 @@
+#include "app_settings.h"
+
+#ifndef SKIP_FOR_CMOCKA
+// NVRAM storage for app settings. Initialized to all zeros by default.
+// Reset on application or OS update.
+const app_settings_t N_app_settings_real;
+#else
+uint8_t mock_allow_nondefault_sighash = 0;
+#endif
diff --git a/src/app_settings.h b/src/app_settings.h
new file mode 100644
index 0000000..6293dcd
--- /dev/null
+++ b/src/app_settings.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * Application settings stored in NVRAM (persistent across reboots, reset on app/OS update).
+ */
+typedef struct {
+ // If true, non-standard sighash types (SIGHASH_NONE, SIGHASH_SINGLE,
+ // SIGHASH_ANYONECANPAY|*) are allowed with a warning during signing.
+ // If false (default), these sighash types are rejected with
+ // SW_SECURITY_STATUS_NOT_SATISFIED.
+ uint8_t allow_nondefault_sighash;
+} app_settings_t;
+
+#ifndef SKIP_FOR_CMOCKA
+
+#include "os.h"
+
+extern const app_settings_t N_app_settings_real;
+#define N_app_settings (*(const volatile app_settings_t *) PIC(&N_app_settings_real))
+
+/**
+ * Returns true if non-standard sighash types are allowed (user opted in through settings).
+ */
+static inline bool app_settings_get_allow_nondefault_sighash(void) {
+ return N_app_settings.allow_nondefault_sighash != 0;
+}
+
+/**
+ * Sets whether non-standard sighash types are allowed.
+ */
+static inline void app_settings_set_allow_nondefault_sighash(bool allow) {
+ uint8_t val = allow ? 1 : 0;
+ nvm_write((void *) &N_app_settings.allow_nondefault_sighash, &val, sizeof(val));
+}
+
+#else /* SKIP_FOR_CMOCKA - unit test stubs */
+
+extern uint8_t mock_allow_nondefault_sighash;
+
+static inline bool app_settings_get_allow_nondefault_sighash(void) {
+ return mock_allow_nondefault_sighash != 0;
+}
+
+static inline void app_settings_set_allow_nondefault_sighash(bool allow) {
+ mock_allow_nondefault_sighash = allow ? 1 : 0;
+}
+
+#endif /* SKIP_FOR_CMOCKA */
diff --git a/src/common/sighash.h b/src/common/sighash.h
new file mode 100644
index 0000000..f83f44c
--- /dev/null
+++ b/src/common/sighash.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <stdint.h>
+
+/* Local headers */
+#include "constants.h"
+
+/**
+ * Classification result for a sighash type.
+ */
+typedef enum {
+ SIGHASH_CLASS_SAFE, // SIGHASH_ALL or SIGHASH_DEFAULT (for segwit v1+)
+ SIGHASH_CLASS_NON_SAFE, // Non-standard but recognized (NONE, SINGLE, ANYONECANPAY|*)
+ SIGHASH_CLASS_UNSUPPORTED // Completely unsupported sighash type
+} sighash_class_t;
+
+/**
+ * Classify a sighash type according to the security model:
+ *
+ * SAFE:
+ * - SIGHASH_DEFAULT (0x00) when segwit_version > 0 (Taproot)
+ * - SIGHASH_ALL (0x01)
+ *
+ * NON_SAFE (requires user opt-in via settings):
+ * - SIGHASH_NONE (0x02)
+ * - SIGHASH_SINGLE (0x03)
+ * - SIGHASH_ANYONECANPAY | SIGHASH_ALL (0x81)
+ * - SIGHASH_ANYONECANPAY | SIGHASH_NONE (0x82)
+ * - SIGHASH_ANYONECANPAY | SIGHASH_SINGLE (0x83)
+ *
+ * UNSUPPORTED: everything else
+ *
+ * @param sighash_type The PSBT_IN_SIGHASH_TYPE value
+ * @param segwit_version The segwit version of the input (-1 for legacy)
+ * @return The classification of the sighash type
+ */
+static inline sighash_class_t classify_sighash(uint32_t sighash_type, int segwit_version) {
+ // SIGHASH_DEFAULT is only valid for Taproot (segwit v1+)
+ if (((segwit_version > 0) && (sighash_type == SIGHASH_DEFAULT)) ||
+ (sighash_type == SIGHASH_ALL)) {
+ return SIGHASH_CLASS_SAFE;
+ }
+
+ if ((segwit_version >= 0) &&
+ ((sighash_type == SIGHASH_NONE) || (sighash_type == SIGHASH_SINGLE) ||
+ (sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_ALL)) ||
+ (sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_NONE)) ||
+ (sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_SINGLE)))) {
+ return SIGHASH_CLASS_NON_SAFE;
+ }
+
+ return SIGHASH_CLASS_UNSUPPORTED;
+}
diff --git a/src/error_codes.h b/src/error_codes.h
index 86ee5de..72b0fbc 100644
--- a/src/error_codes.h
+++ b/src/error_codes.h
@@ -64,6 +64,10 @@
// The wallet policy has too many internal keys.
#define EC_SIGN_PSBT_WALLET_POLICY_TOO_MANY_INTERNAL_KEYS 0x000c
+// Non-standard sighash types are not allowed unless the user has explicitly enabled them
+// in the application settings. Enable "Allow non-standard sighash" in the app settings to proceed.
+#define EC_SIGN_PSBT_NONDEFAULT_SIGHASH_NOT_ALLOWED 0x000d
+
/**
* Swap
*/
diff --git a/src/handler/sign_psbt/preprocess_inputs.c b/src/handler/sign_psbt/preprocess_inputs.c
index 59bfc56..0d04dec 100644
--- a/src/handler/sign_psbt/preprocess_inputs.c
+++ b/src/handler/sign_psbt/preprocess_inputs.c
@@ -24,6 +24,7 @@
#include "read.h"
/* Local headers */
+#include "app_settings.h"
#include "amount_from_psbt.h"
#include "bitvector.h"
#include "buffer.h"
@@ -36,6 +37,7 @@
#include "policy.h"
#include "process_in_outs.h"
#include "psbt.h"
+#include "sighash.h"
#include "sign_psbt_cache.h"
#include "sw.h"
@@ -289,16 +291,20 @@ bool __attribute__((noinline)) preprocess_inputs(
return false;
}
- if (((segwit_version > 0) && (input.sighash_type == SIGHASH_DEFAULT)) ||
- (input.sighash_type == SIGHASH_ALL)) {
+ sighash_class_t sighash_class = classify_sighash(input.sighash_type, segwit_version);
+ if (sighash_class == SIGHASH_CLASS_SAFE) {
PRINTF("Sighash type is SIGHASH_DEFAULT or SIGHASH_ALL\n");
- } else if ((segwit_version >= 0) &&
- ((input.sighash_type == SIGHASH_NONE) ||
- (input.sighash_type == SIGHASH_SINGLE) ||
- (input.sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_ALL)) ||
- (input.sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_NONE)) ||
- (input.sighash_type == (SIGHASH_ANYONECANPAY | SIGHASH_SINGLE)))) {
+ } else if (sighash_class == SIGHASH_CLASS_NON_SAFE) {
+ // Non-standard sighash: only allowed if the user explicitly enabled
+ // "Allow non-standard sighash" in the application settings.
+ if (!app_settings_get_allow_nondefault_sighash()) {
+ PRINTF("Non-standard sighash rejected: setting not enabled\n");
+ SEND_SW_EC(dc,
+ SW_SECURITY_STATUS_NOT_SATISFIED,
+ EC_SIGN_PSBT_NONDEFAULT_SIGHASH_NOT_ALLOWED);
+ return false;
+ }
PRINTF("Sighash type is non-default, will show a warning.\n");
st->warnings.non_default_sighash = true;
} else {
diff --git a/src/ui/menu_nbgl.c b/src/ui/menu_nbgl.c
index 8f561ed..8a3f0c0 100644
--- a/src/ui/menu_nbgl.c
+++ b/src/ui/menu_nbgl.c
@@ -19,9 +19,15 @@
#include "nbgl_use_case.h"
/* Local headers */
+#include "app_settings.h"
#include "display.h"
#include "menu.h"
+// Tokens for settings switches
+enum {
+ ALLOW_NONDEFAULT_SIGHASH_TOKEN = FIRST_USER_TOKEN,
+};
+
#define SETTING_INFO_NB 3
static const char* const INFO_TYPES[SETTING_INFO_NB] = {"Version", "Developer", "Copyright"};
static const char* const INFO_CONTENTS[SETTING_INFO_NB] = {APPVERSION, "Ledger", "(c) 2026 Ledger"};
@@ -32,9 +38,85 @@ static const nbgl_contentInfoList_t infoList = {
.infoContents = INFO_CONTENTS,
};
+// Settings switch descriptor (mutable so initState can be updated)
+static nbgl_contentSwitch_t settingsSwitches[1];
+
+// Saved settings page index, so we can restore it after a confirmation dialog
+static uint8_t initSettingPage;
+
+static void enable_sighash_choice_callback(bool confirm);
+static void settings_controls_callback(int token, uint8_t index, int page);
+
+static const nbgl_content_t settingsContentsList[] = {{
+ .type = SWITCHES_LIST,
+ .content.switchesList.nbSwitches = 1,
+ .content.switchesList.switches = settingsSwitches,
+ .contentActionCallback = settings_controls_callback,
+}};
+
+static const nbgl_genericContents_t settingsContents = {
+ .callbackCallNeeded = false,
+ .contentsList = settingsContentsList,
+ .nbContents = 1,
+};
+
extern void app_exit(void);
-void ui_menu_main(void) {
+// Forward-declare so the confirmation callback can re-display the home/settings
+void ui_menu_main(void);
+void ui_menu_main_with_settings_page(uint8_t settingsPage);
+
+// Callback for the "are you sure?" confirmation dialog when enabling non-standard sighash
+static void enable_sighash_choice_callback(bool confirm) {
+ if (confirm) {
+ app_settings_set_allow_nondefault_sighash(true);
+ settingsSwitches[0].initState = ON_STATE;
+ }
+ // Re-display the home + settings (returning to the settings page)
+ ui_menu_main_with_settings_page(initSettingPage);
+}
+
+static void settings_controls_callback(int token, uint8_t index, int page) {
+ UNUSED(index);
+
+ initSettingPage = page;
+
+ if (token == ALLOW_NONDEFAULT_SIGHASH_TOKEN) {
+ if (!app_settings_get_allow_nondefault_sighash()) {
+ // About to enable: show a warning confirmation dialog
+ nbgl_useCaseChoice(&ICON_APP_WARNING,
+#ifdef SCREEN_SIZE_WALLET
+ "Non-standard sighash",
+#else
+ "Sighash types",
+#endif
+ "This allows signing transactions that leave parts "
+ "unprotected. You'll still confirm each one.",
+ "I understand, enable",
+ "Cancel",
+ enable_sighash_choice_callback);
+ } else {
+ // Disabling: no confirmation needed
+ app_settings_set_allow_nondefault_sighash(false);
+ settingsSwitches[0].initState = OFF_STATE;
+ }
+ }
+}
+
+void ui_menu_main_with_settings_page(uint8_t settingsPage) {
+ // Initialize settings switch state from NVRAM
+ settingsSwitches[0] = (nbgl_contentSwitch_t) {
+#ifdef SCREEN_SIZE_WALLET
+ .text = "Non-standard sighash",
+ .subText = "Allow non-standard signing rules with warning",
+#else
+ .text = "Sighash types",
+ .subText = "Allow non-default sighash types",
+#endif
+ .initState = app_settings_get_allow_nondefault_sighash() ? ON_STATE : OFF_STATE,
+ .token = ALLOW_NONDEFAULT_SIGHASH_TOKEN,
+ };
+
nbgl_useCaseHomeAndSettings(
#if BIP44_COIN_TYPE == 1
"Bitcoin Testnet",
@@ -50,9 +132,13 @@ void ui_menu_main(void) {
#else
NULL,
#endif /* #ifdef BITCOIN_RECOVERY */
- INIT_HOME_PAGE,
- NULL,
+ settingsPage,
+ &settingsContents,
&infoList,
NULL,
app_exit);
}
+
+void ui_menu_main(void) {
+ ui_menu_main_with_settings_page(INIT_HOME_PAGE);
+}
Why this scored 48/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.