hsmd: find correct P2TR key for utxo
What changed, and why it matters
This commit fixes a bug in Core Lightning's HSM (Hardware Security Module daemon) where the wrong private key could be derived when spending a taproot (P2TR) UTXO. Previously, the code always used the older BIP32-style derivation for taproot UTXOs. After the change, it checks whether the UTXO was actually created using the newer BIP86 derivation path (used with mnemonic-based wallets) and, if so, derives the correct key. If the wrong key is used, the node would be unable to sign a transaction spending that UTXO, effectively locking the funds until the bug is fixed. There is no direct evidence in the commit or supplied references that this was a security vulnerability exploitable by an attacker; it appears to be a correctness/functional bug.
Treat as a functional correctness fix rather than an active security vulnerability. Operators using mnemonic/BIP86 wallets with taproot UTXOs should upgrade to avoid being unable to spend funds. No immediate incident-response action is indicated by the supplied materials.
Security signals we found
Incorrect key derivation for P2TR UTXOs could prevent signing and spending
Fix distinguishes BIP86 vs BIP32 taproot derivation paths
No explicit security impact described by vendor in commit message
No CVE, advisory, or researcher attribution present in supplied materials
Evidence from the diff
The patch modifies hsm_key_for_utxo() in hsmd/libhsmd.c to detect whether a P2TR UTXO was derived via BIP86 rather than the legacy BIP32 path. It does this by deriving the public key with bip86_key(), constructing the corresponding P2TR scriptPubKey, and comparing it to the UTXO’s scriptPubKey. If they match, it uses bip86_key() to derive the signing key; otherwise it falls back to bitcoin_key() (BIP32). Helper functions is_mnemonic_secret() and use_bip86_derivation() are added in common/hsm_secret.c/h and used in hsmd/hsmd.c to replace a raw 64-byte length check. The change is localized to key derivation for P2TR UTXOs and only affects wallets using the new mnemonic/BIP86 support.
Changed components
hsmd/libhsmd.chsmd/hsmd.ccommon/hsm_secret.ccommon/hsm_secret.hInspect captured patch +54 / −4
diff --git a/common/hsm_secret.c b/common/hsm_secret.c
index f377d91..f14c7e1 100644
--- a/common/hsm_secret.c
+++ b/common/hsm_secret.c
@@ -557,3 +557,14 @@ size_t hsm_secret_size(const struct hsm_secret *hsm)
return tal_bytelen(hsm->secret_data);
return sizeof(hsm->secret);
}
+
+bool is_mnemonic_secret(size_t secret_len)
+{
+ return secret_len == HSM_SECRET_MNEMONIC_SIZE;
+}
+
+bool use_bip86_derivation(size_t secret_len)
+{
+ /* BIP86 was introduced alongside mnemonic support, so they're available together */
+ return is_mnemonic_secret(secret_len);
+}
diff --git a/common/hsm_secret.h b/common/hsm_secret.h
index d1d1f56..66c158f 100644
--- a/common/hsm_secret.h
+++ b/common/hsm_secret.h
@@ -57,6 +57,19 @@ const u8 *hsm_secret_bytes(const struct hsm_secret *hsm);
*/
size_t hsm_secret_size(const struct hsm_secret *hsm);
+/**
+ * Check if this HSM secret is mnemonic-based (64-byte seed).
+ * Returns true for mnemonic-derived secrets, false for legacy 32-byte secrets.
+ */
+bool is_mnemonic_secret(size_t secret_len);
+
+/**
+ * Check if we should use BIP86 derivation for this HSM secret.
+ * BIP86 was introduced alongside mnemonic support, so they're available together.
+ * Returns true if mnemonic-based secret is available, false otherwise.
+ */
+bool use_bip86_derivation(size_t secret_len);
+
/**
* Checks whether the hsm_secret data requires a passphrase to decrypt.
* Handles legacy, encrypted, and mnemonic-based formats.
diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c
index 616d7fe..5e23065 100644
--- a/hsmd/hsmd.c
+++ b/hsmd/hsmd.c
@@ -696,7 +696,7 @@ static struct io_plan *handle_derive_bip86_key(struct io_conn *conn,
return bad_req(conn, c, msg_in);
/* Check if we have a mnemonic-based HSM secret */
- if (hsm_secret_size(&hsm_secret) != 64) {
+ if (!use_bip86_derivation(hsm_secret_size(&hsm_secret))) {
return bad_req_fmt(conn, c, msg_in,
"BIP86 derivation requires mnemonic-based HSM secret");
}
@@ -724,7 +724,7 @@ static struct io_plan *handle_check_bip86_pubkey(struct io_conn *conn,
return bad_req(conn, c, msg_in);
/* Check if we have a mnemonic-based HSM secret */
- if (hsm_secret_size(&hsm_secret) != 64) {
+ if (!use_bip86_derivation(hsm_secret_size(&hsm_secret))) {
return bad_req_fmt(conn, c, msg_in,
"BIP86 derivation requires mnemonic-based HSM secret");
}
diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c
index b5b4f1c..b48c15d 100644
--- a/hsmd/libhsmd.c
+++ b/hsmd/libhsmd.c
@@ -2,10 +2,12 @@
#include <bitcoin/script.h>
#include <ccan/array_size/array_size.h>
#include <ccan/crypto/hkdf_sha256/hkdf_sha256.h>
+#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/bolt12_id.h>
#include <common/bolt12_merkle.h>
#include <common/hash_u5.h>
+#include <common/hsm_secret.h>
#include <common/key_derive.h>
#include <common/lease_rates.h>
#include <common/memleak.h>
@@ -542,8 +544,32 @@ static void hsm_key_for_utxo(struct privkey *privkey, struct pubkey *pubkey,
hsmd_status_debug("Derived public key %s from unilateral close",
fmt_pubkey(tmpctx, pubkey));
} else {
- /* Simple case: just get derive via HD-derivation */
- bitcoin_key(privkey, pubkey, utxo->keyindex);
+ /* Check if this is a BIP86 UTXO by examining the scriptPubkey */
+ const size_t script_len = tal_bytelen(utxo->scriptPubkey);
+ bool is_bip86 = false;
+
+ /* For P2TR scripts, we need to determine if it's BIP86 or regular P2TR
+ * But BIP86 derivation requires mnemonic-based secrets */
+ if (is_p2tr(utxo->scriptPubkey, script_len, NULL) &&
+ use_bip86_derivation(secretstuff.bip32_seed_len)) {
+ /* Try BIP86 derivation first and see if it matches */
+ struct pubkey test_pubkey;
+ bip86_key(NULL, &test_pubkey, utxo->keyindex);
+
+ /* Create P2TR scriptpubkey from BIP86 key and compare */
+ const u8 *bip86_script = scriptpubkey_p2tr(tmpctx, &test_pubkey);
+ if (memeq(utxo->scriptPubkey, script_len, bip86_script, tal_bytelen(bip86_script))) {
+ is_bip86 = true;
+ }
+ }
+
+ if (is_bip86) {
+ /* Use BIP86 derivation */
+ bip86_key(privkey, pubkey, utxo->keyindex);
+ } else {
+ /* Simple case: just get derive via HD-derivation */
+ bitcoin_key(privkey, pubkey, utxo->keyindex);
+ }
}
}
Why this scored 42/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.