Special handling for get_extended_pubkey_at_path() distinguishing error codes
What changed, and why it matters
This commit changes how the Ledger Bitcoin app reports failures when deriving public keys from a BIP32 path. Previously, almost any derivation failure was reported as a generic 'bad state' error. Now, the app distinguishes between different underlying error codes, specifically mapping two SDK error codes (0x4212 and 0x4215) to a 'not supported' status word instead of 'bad state'. This is a defensive hardening change: it gives callers more accurate error information and avoids misleading 'bad state' responses for permission or policy-related derivation failures. There is no direct evidence in the commit of an exploitable vulnerability being fixed.
Treat as a defensive hardening commit. Review whether any client software or tests depend on the previous SW_BAD_STATE response for derivation failures and update them to expect SW_NOT_SUPPORTED where appropriate. No urgent security patch is indicated by the diff alone.
Security signals we found
Error-code propagation improvement for BIP32 key derivation failures
Mapping of SDK error 0x4212 (root derivation without master permission) to SW_NOT_SUPPORTED
Mapping of SDK error 0x4215 (unauthorized derivation path) to SW_NOT_SUPPORTED
Replacement of generic SW_BAD_STATE with more specific status words for derivation errors
Defensive hardening in public-key derivation used across wallet registration, PSBT signing, and swap address checks
Evidence from the diff
The patch refactors crypto_get_compressed_pubkey_at_path() and get_extended_pubkey_at_path() to return cx_err_t (the SDK’s cryptographic error code) instead of bool/int. A new helper cx_err_to_sw() maps cx_err_t values to APDU status words (SW). Notably, SDK errors 0x4212 (missing HAVE_APPLICATION_FLAG_DERIVE_MASTER permission for root-level derivation) and 0x4215 (unauthorized BIP32 path) are mapped to SW_NOT_SUPPORTED rather than SW_BAD_STATE. Call sites in get_extended_pubkey, register_wallet, sign_psbt, policy validation, and swap check_address are updated to handle the new return type. The change improves error transparency and prevents incorrect error classification, but does not alter the actual derivation policy or permissions.
Changed components
src/crypto.csrc/crypto.hsrc/handler/get_extended_pubkey.csrc/handler/lib/policy.csrc/handler/register_wallet.csrc/handler/sign_psbt.csrc/swap/handle_check_address.cInspect captured patch +94 / −62
diff --git a/src/crypto.c b/src/crypto.c
index 32f477a..d827748 100644
--- a/src/crypto.c
+++ b/src/crypto.c
@@ -35,6 +35,7 @@
#include "common/read.h"
#include "common/write.h"
+#include "../boilerplate/sw.h"
#include "../debug-helpers/debug.h"
#include "crypto.h"
@@ -241,26 +242,28 @@ void crypto_get_checksum(const uint8_t *in, uint16_t in_len, uint8_t out[static
memmove(out, buffer, 4);
}
-bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[],
- uint8_t bip32_path_len,
- uint8_t pubkey[static 33],
- uint8_t chain_code[]) {
+cx_err_t crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[],
+ uint8_t bip32_path_len,
+ uint8_t pubkey[static 33],
+ uint8_t chain_code[]) {
uint8_t raw_public_key[65];
+ cx_err_t error = CX_OK;
- if (bip32_derive_get_pubkey_256(CX_CURVE_256K1,
- bip32_path,
- bip32_path_len,
- raw_public_key,
- chain_code,
- CX_SHA512) != CX_OK) {
- return false;
+ error = bip32_derive_get_pubkey_256(CX_CURVE_256K1,
+ bip32_path,
+ bip32_path_len,
+ raw_public_key,
+ chain_code,
+ CX_SHA512);
+ if (error != CX_OK) {
+ return error;
}
if (crypto_get_compressed_pubkey(raw_public_key, pubkey) < 0) {
- return false;
+ return CX_INTERNAL_ERROR;
}
- return true;
+ return error;
}
uint32_t crypto_get_key_fingerprint(const uint8_t pub_key[static 33]) {
@@ -315,23 +318,26 @@ bool crypto_derive_symmetric_key(const char *label, size_t label_len, uint8_t ke
return ret == CX_OK;
}
-int get_extended_pubkey_at_path(const uint32_t bip32_path[],
- uint8_t bip32_path_len,
- uint32_t bip32_pubkey_version,
- serialized_extended_pubkey_t *out_pubkey) {
+cx_err_t get_extended_pubkey_at_path(const uint32_t bip32_path[],
+ uint8_t bip32_path_len,
+ uint32_t bip32_pubkey_version,
+ serialized_extended_pubkey_t *out_pubkey) {
// find parent key's fingerprint and child number
uint32_t parent_fingerprint = 0;
uint32_t child_number = 0;
+ cx_err_t error = CX_OK;
+
if (bip32_path_len > 0) {
// here we reuse the storage for the parent keys that we will later use
// for the response, in order to save memory
uint8_t parent_pubkey[33];
- if (!crypto_get_compressed_pubkey_at_path(bip32_path,
- bip32_path_len - 1,
- parent_pubkey,
- NULL)) {
- return -1;
+ error = crypto_get_compressed_pubkey_at_path(bip32_path,
+ bip32_path_len - 1,
+ parent_pubkey,
+ NULL);
+ if (error != CX_OK) {
+ return error;
}
parent_fingerprint = crypto_get_key_fingerprint(parent_pubkey);
@@ -343,14 +349,31 @@ int get_extended_pubkey_at_path(const uint32_t bip32_path[],
write_u32_be(out_pubkey->parent_fingerprint, 0, parent_fingerprint);
write_u32_be(out_pubkey->child_number, 0, child_number);
- if (!crypto_get_compressed_pubkey_at_path(bip32_path,
- bip32_path_len,
- out_pubkey->compressed_pubkey,
- out_pubkey->chain_code)) {
- return -1;
+ return crypto_get_compressed_pubkey_at_path(bip32_path,
+ bip32_path_len,
+ out_pubkey->compressed_pubkey,
+ out_pubkey->chain_code);
+}
+
+uint16_t cx_err_to_sw(cx_err_t error) {
+ if (error == CX_OK) {
+ return SW_OK;
}
- return 0;
+ /* The error codes are not currently defined in the SDK */
+ if (error == 0x4212) {
+ PRINTF(
+ "Attempt to derive a key at root level without "
+ "HAVE_APPLICATION_FLAG_DERIVE_MASTER permission.\n");
+ return SW_NOT_SUPPORTED;
+ }
+ if (error == 0x4215) {
+ PRINTF("Attempt to derive a key at unauthorized path.\n");
+ return SW_NOT_SUPPORTED;
+ }
+
+ PRINTF("Failed getting bip32 pubkey, error = 0x%08X\n", error);
+ return SW_BAD_STATE;
}
int base58_encode_address(const uint8_t in[20], uint32_t version, char *out, size_t out_len) {
diff --git a/src/crypto.h b/src/crypto.h
index 2248365..9e24aee 100644
--- a/src/crypto.h
+++ b/src/crypto.h
@@ -252,10 +252,10 @@ void crypto_get_checksum(const uint8_t *in, uint16_t in_len, uint8_t out[static
*
* @return true on success, false in case of error.
*/
-bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[],
- uint8_t bip32_path_len,
- uint8_t pubkey[static 33],
- uint8_t chain_code[]);
+cx_err_t crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[],
+ uint8_t bip32_path_len,
+ uint8_t pubkey[static 33],
+ uint8_t chain_code[]);
/**
* Computes the fingerprint of a compressed key as per BIP32; that is, the first 4 bytes of the
@@ -289,10 +289,10 @@ uint32_t crypto_get_master_key_fingerprint();
*
* @return 0 on success, or -1 on error.
*/
-int get_extended_pubkey_at_path(const uint32_t bip32_path[],
- uint8_t bip32_path_len,
- uint32_t bip32_pubkey_version,
- serialized_extended_pubkey_t *out_pubkey);
+cx_err_t get_extended_pubkey_at_path(const uint32_t bip32_path[],
+ uint8_t bip32_path_len,
+ uint32_t bip32_pubkey_version,
+ serialized_extended_pubkey_t *out_pubkey);
/**
* Derives the level-1 symmetric key at the given label using SLIP-0021.
@@ -474,3 +474,13 @@ int crypto_tr_tweak_seckey(const uint8_t seckey[static 32],
const uint8_t *h,
size_t h_len,
uint8_t out[static 32]);
+
+/**
+ * Converts cx_err_t to 2-bytes SW and prints out debug information.
+ *
+ * @param[in] error
+ * Cryptographic error code
+ *
+ * @return SW (SW_OK on success, other value on error).
+ */
+uint16_t cx_err_to_sw(cx_err_t error);
diff --git a/src/handler/get_extended_pubkey.c b/src/handler/get_extended_pubkey.c
index e9c6318..143ce98 100644
--- a/src/handler/get_extended_pubkey.c
+++ b/src/handler/get_extended_pubkey.c
@@ -137,12 +137,13 @@ void handler_get_extended_pubkey(dispatcher_context_t *dc, uint8_t protocol_vers
}
serialized_extended_pubkey_check_t pubkey_check;
- if (0 > get_extended_pubkey_at_path(bip32_path,
- bip32_path_len,
- BIP32_PUBKEY_VERSION,
- &pubkey_check.serialized_extended_pubkey)) {
- PRINTF("Failed getting bip32 pubkey\n");
- SEND_SW(dc, SW_BAD_STATE);
+ uint16_t sw =
+ cx_err_to_sw(get_extended_pubkey_at_path(bip32_path,
+ bip32_path_len,
+ BIP32_PUBKEY_VERSION,
+ &pubkey_check.serialized_extended_pubkey));
+ if (SW_OK != sw) {
+ SEND_SW(dc, sw);
return;
}
diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c
index 0a1b30b..cc93be6 100644
--- a/src/handler/lib/policy.c
+++ b/src/handler/lib/policy.c
@@ -1478,10 +1478,10 @@ bool is_wallet_policy_standard(dispatcher_context_t *dispatcher_context,
// generate pubkey and check if it matches
serialized_extended_pubkey_t derived_pubkey;
- if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation,
- key_info.master_key_derivation_len,
- BIP32_PUBKEY_VERSION,
- &derived_pubkey)) {
+ if (CX_OK != get_extended_pubkey_at_path(key_info.master_key_derivation,
+ key_info.master_key_derivation_len,
+ BIP32_PUBKEY_VERSION,
+ &derived_pubkey)) {
PRINTF("Failed to derive pubkey\n");
return false;
}
diff --git a/src/handler/register_wallet.c b/src/handler/register_wallet.c
index 4bb3899..3339fc4 100644
--- a/src/handler/register_wallet.c
+++ b/src/handler/register_wallet.c
@@ -193,13 +193,13 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version)
read_u32_be(key_info.master_key_fingerprint, 0) == master_key_fingerprint) {
// we verify that we can actually generate the same pubkey
serialized_extended_pubkey_t pubkey_derived;
- int serialized_pubkey_len =
- get_extended_pubkey_at_path(key_info.master_key_derivation,
- key_info.master_key_derivation_len,
- BIP32_PUBKEY_VERSION,
- &pubkey_derived);
- if (serialized_pubkey_len == -1) {
- SEND_SW(dc, SW_BAD_STATE);
+ uint16_t sw =
+ cx_err_to_sw(get_extended_pubkey_at_path(key_info.master_key_derivation,
+ key_info.master_key_derivation_len,
+ BIP32_PUBKEY_VERSION,
+ &pubkey_derived));
+ if (SW_OK != sw) {
+ SEND_SW(dc, sw);
return;
}
diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c
index ad0086f..865d41f 100644
--- a/src/handler/sign_psbt.c
+++ b/src/handler/sign_psbt.c
@@ -399,10 +399,10 @@ static bool __attribute__((noinline)) get_and_verify_key_info(dispatcher_context
// it could be a collision on the fingerprint; we verify that we can actually generate
// the same pubkey
serialized_extended_pubkey_t derived_pubkey;
- if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation,
- key_info.master_key_derivation_len,
- BIP32_PUBKEY_VERSION,
- &derived_pubkey)) {
+ if (CX_OK != get_extended_pubkey_at_path(key_info.master_key_derivation,
+ key_info.master_key_derivation_len,
+ BIP32_PUBKEY_VERSION,
+ &derived_pubkey)) {
return false;
}
diff --git a/src/swap/handle_check_address.c b/src/swap/handle_check_address.c
index c72b988..21f23d2 100644
--- a/src/swap/handle_check_address.c
+++ b/src/swap/handle_check_address.c
@@ -110,10 +110,8 @@ int handle_check_address(check_address_parameters_t* params) {
return false;
}
- if (!crypto_get_compressed_pubkey_at_path(path.path,
- path.length,
- compressed_public_key,
- NULL)) {
+ if (CX_OK !=
+ crypto_get_compressed_pubkey_at_path(path.path, path.length, compressed_public_key, NULL)) {
return 0;
}
char address[MAX_ADDRESS_LENGTH_STR + 1];
@@ -133,4 +131,4 @@ int handle_check_address(check_address_parameters_t* params) {
}
PRINTF("Addresses match\n");
return 1;
-}
\ No newline at end of file
+}
Why this scored 38/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.