Zeroing out nonce-related stack buffers in musig part
What changed, and why it matters
This commit is a defensive cleanup for the MuSig2 multi-signature code in Ledger's Bitcoin app. It makes sure that temporary memory holding secret nonce values and randomness is wiped clean even when things go wrong, not just on the happy path. The change reduces the chance that sensitive signing material could be left behind in device memory after an error, but it does not appear to fix an active remote exploit by itself.
Treat as a security-hardening patch and include it in the next release. Review whether other MuSig2 and signing code paths have similar cleanup gaps. No urgent incident response is indicated by the diff alone.
Security signals we found
explicit_bzero added to failure paths in MuSig2 nonce generation and signing
early returns replaced with centralized cleanup to prevent secret material lingering on stack
secret-derived local variables (rand_i_j, secnonce, k_1, k_2, bk_2) now zeroed on all exit paths
no change to cryptographic algorithms or protocol state machine
Evidence from the diff
The patch adds explicit_bzero calls for rand_i_j, secnonce, and related stack buffers in the MuSig2 signing flow, including failure paths inside musig_nonce_gen and musig_sign. It refactors early returns into goto-based cleanup so that secret-derived stack variables are cleared before returning an error. This is a side-channel / secure-cleanup hardening change rather than a logic bug fix.
Changed components
src/handler/sign_psbt/musig_signing.csrc/musig/musig.cInspect captured patch +59 / −26
diff --git a/src/handler/sign_psbt/musig_signing.c b/src/handler/sign_psbt/musig_signing.c
index 122665e..07d80ca 100644
--- a/src/handler/sign_psbt/musig_signing.c
+++ b/src/handler/sign_psbt/musig_signing.c
@@ -269,20 +269,22 @@ bool produce_and_yield_pubnonce(dispatcher_context_t *dc,
return false;
}
+ bool ret = false;
uint8_t rand_i_j[32];
compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j);
musig_secnonce_t secnonce;
musig_pubnonce_t pubnonce;
- if (0 > musig_nonce_gen(rand_i_j,
- sizeof(rand_i_j),
- keyexpr_info->internal_pubkey.compressed_pubkey,
- musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1,
- &secnonce,
- &pubnonce)) {
+ int res = musig_nonce_gen(rand_i_j,
+ sizeof(rand_i_j),
+ keyexpr_info->internal_pubkey.compressed_pubkey,
+ musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1,
+ &secnonce,
+ &pubnonce);
+ explicit_bzero(&secnonce, sizeof(secnonce));
+ if (0 > res) {
PRINTF("MuSig2 nonce generation failed\n");
- SEND_SW(dc, SW_BAD_STATE); // should never happen
- return false;
+ goto cleanup;
}
if (!yield_musig_pubnonce(dc,
@@ -293,6 +295,14 @@ bool produce_and_yield_pubnonce(dispatcher_context_t *dc,
musig_per_input_info.agg_key_tweaked.compressed_pubkey,
keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) {
PRINTF("Failed yielding MuSig2 pubnonce\n");
+ goto cleanup;
+ }
+
+ ret = true;
+
+cleanup:
+ explicit_bzero(rand_i_j, sizeof(rand_i_j));
+ if (!ret) {
SEND_SW(dc, SW_BAD_STATE); // should never happen
return false;
}
@@ -413,6 +423,8 @@ bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t
&secnonce,
&pubnonce)) {
PRINTF("MuSig2 nonce generation failed\n");
+ explicit_bzero(rand_i_j, sizeof(rand_i_j));
+ explicit_bzero(&secnonce, sizeof(secnonce));
SEND_SW(dc, SW_BAD_STATE); // should never happen
return false;
}
@@ -464,6 +476,8 @@ bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t
} while (false);
explicit_bzero(&private_key, sizeof(private_key));
+ explicit_bzero(rand_i_j, sizeof(rand_i_j));
+ explicit_bzero(&secnonce, sizeof(secnonce));
if (err) {
PRINTF("Partial signature generation failed\n");
diff --git a/src/musig/musig.c b/src/musig/musig.c
index 48982f2..635aa56 100644
--- a/src/musig/musig.c
+++ b/src/musig/musig.c
@@ -264,27 +264,32 @@ int musig_nonce_gen(const uint8_t *rand,
uint8_t msg[] = {0x00};
musig_nonce_hash(rand, rand_len, pk, aggpk, 0, msg, 1, NULL, 0, secnonce->k_1);
- if (CX_OK != cx_math_modm_no_throw(secnonce->k_1, 32, secp256k1_n, 32)) return -1;
+ if (CX_OK != cx_math_modm_no_throw(secnonce->k_1, 32, secp256k1_n, 32)) goto nonce_gen_fail;
musig_nonce_hash(rand, rand_len, pk, aggpk, 1, msg, 1, NULL, 0, secnonce->k_2);
- if (CX_OK != cx_math_modm_no_throw(secnonce->k_2, 32, secp256k1_n, 32)) return -1;
+ if (CX_OK != cx_math_modm_no_throw(secnonce->k_2, 32, secp256k1_n, 32)) goto nonce_gen_fail;
if (is_array_all_zeros(secnonce->k_1, sizeof(secnonce->k_1)) ||
is_array_all_zeros(secnonce->k_2, sizeof(secnonce->k_2))) {
// this can only happen with negligible probability
- return -1;
+ goto nonce_gen_fail;
}
memcpy(secnonce->pk, pk, sizeof(secnonce->pk));
point_t R_s1, R_s2;
- if (CX_OK != point_mul(G, secnonce->k_1, &R_s1)) return -1;
- if (CX_OK != point_mul(G, secnonce->k_2, &R_s2)) return -1;
+ if (CX_OK != point_mul(G, secnonce->k_1, &R_s1)) goto nonce_gen_fail;
+ if (CX_OK != point_mul(G, secnonce->k_2, &R_s2)) goto nonce_gen_fail;
- if (0 > crypto_get_compressed_pubkey(R_s1.raw, pubnonce->R_s1)) return -1;
- if (0 > crypto_get_compressed_pubkey(R_s2.raw, pubnonce->R_s2)) return -1;
+ if (0 > crypto_get_compressed_pubkey(R_s1.raw, pubnonce->R_s1)) goto nonce_gen_fail;
+ if (0 > crypto_get_compressed_pubkey(R_s2.raw, pubnonce->R_s2)) goto nonce_gen_fail;
return 0;
+
+nonce_gen_fail:
+ explicit_bzero(secnonce->k_1, sizeof(secnonce->k_1));
+ explicit_bzero(secnonce->k_2, sizeof(secnonce->k_2));
+ return -1;
}
int musig_nonce_agg(const musig_pubnonce_t pubnonces[], size_t n_keys, musig_pubnonce_t *out) {
@@ -484,40 +489,49 @@ int musig_sign(musig_secnonce_t *secnonce,
explicit_bzero(secnonce->k_1, sizeof(secnonce->k_1));
explicit_bzero(secnonce->k_2, sizeof(secnonce->k_2));
+ bool err = false;
+ uint8_t bk_2[32];
+
if (CX_OK != cx_math_cmp_no_throw(k_1, secp256k1_n, 32, &diff)) {
- return -1;
+ err = true;
+ goto cleanup;
}
if (is_array_all_zeros(k_1, sizeof(k_1)) || diff >= 0) {
PRINTF("first secnonce value is out of range\n");
- return -1;
+ err = true;
+ goto cleanup;
}
if (CX_OK != cx_math_cmp_no_throw(k_2, secp256k1_n, 32, &diff)) {
- return -1;
+ err = true;
+ goto cleanup;
}
if (is_array_all_zeros(k_2, sizeof(k_2)) || diff >= 0) {
PRINTF("second secnonce value is out of range\n");
- return -1;
+ err = true;
+ goto cleanup;
}
if (!has_even_y(&R)) {
if (CX_OK != cx_math_sub_no_throw(k_1, secp256k1_n, k_1, 32)) {
- return -1;
+ err = true;
+ goto cleanup;
};
if (CX_OK != cx_math_sub_no_throw(k_2, secp256k1_n, k_2, 32)) {
- return -1;
+ err = true;
+ goto cleanup;
};
}
if (CX_OK != cx_math_cmp_no_throw(sk, secp256k1_n, 32, &diff)) {
- return -1;
+ err = true;
+ goto cleanup;
}
if (is_array_all_zeros(sk, 32) || diff >= 0) {
PRINTF("secret key value is out of range\n");
- return -1;
+ err = true;
+ goto cleanup;
}
- bool err = false;
-
// Put together all the variables that we want to always zero out before returning.
// As an excess of safety, we put here any variable that is (directly or indirectly) derived
// from the secret during the computation of the signature
@@ -574,7 +588,7 @@ int musig_sign(musig_secnonce_t *secnonce,
break;
}
- uint8_t bk_2[32]; // b * k_2
+ // bk_2 = b * k_2
if (CX_OK != cx_math_multm_no_throw(bk_2, b, k_2, secp256k1_n, 32)) {
err = true;
break;
@@ -607,6 +621,11 @@ int musig_sign(musig_secnonce_t *secnonce,
// make sure to zero out any variable derived from secrets before returning
explicit_bzero(&secrets, sizeof(secrets));
+cleanup:
+ explicit_bzero(k_1, sizeof(k_1));
+ explicit_bzero(k_2, sizeof(k_2));
+ explicit_bzero(bk_2, sizeof(bk_2));
+
if (err) {
return -1;
}
Why this scored 58/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.