hsmd_wire: remove hsmd_derive_bip86_key and add it to hsmd_init_reply_v4
What changed, and why it matters
This commit refactors how Core Lightning's Hardware Security Module (HSM) provides a BIP86 base key. Previously, the main daemon asked the HSM for the key via a separate request after initialization. Now, the HSM includes the key directly in its initialization reply when the secret is mnemonic-based. This is a design cleanup that reduces the number of HSM messages and removes a dedicated key-derivation wire command. There is no direct evidence in the commit that this fixes an active security vulnerability, but it does reduce the attack surface by removing one HSM interface that exposed a sensitive extended key.
Treat as a defensive hardening/cleanup change. Reviewers should verify that the bip86_base TLV is only populated for mnemonic-based HSM secrets, that legacy secrets do not receive the key, and that no other code paths still expect the removed WIRE_HSMD_DERIVE_BIP86_KEY message. No immediate incident response is indicated absent additional disclosure context.
Security signals we found
Removal of an HSM wire command that returned a sensitive extended key (BIP86 base)
Reduction in number of synchronous HSM round-trips that expose key material
TLV-based key delivery tied to mnemonic secret type, reducing unconditional exposure
Simplification of P2TR UTXO key derivation logic, removing scriptPubkey probing
Evidence from the diff
The patch removes the WIRE_HSMD_DERIVE_BIP86_KEY request/reply pair and instead embeds the bip86_base extended key as a TLV field (bip86_base, type 2) inside hsmd_init_reply_v4_tlvs. The HSM now derives and returns the BIP86 base key during init only when use_bip86_derivation() is true (mnemonic-based secrets). lightningd/hsm_control.c consumes the TLV directly rather than issuing a follow-up synchronous HSM request. The hsm_key_for_utxo logic is simplified to assume BIP86 for P2TR UTXOs when mnemonic secrets are in use, removing the scriptPubkey comparison fallback. A new HSM version hash is added for ‘v6 with bip86_base in TLV’.
Changed components
hsmd/hsmd.chsmd/libhsmd.chsmd/hsmd_wire.csvlightningd/hsm_control.ccommon/hsm_version.hwallet/test/run-wallet.cInspect captured patch +17 / −92
diff --git a/common/hsm_version.h b/common/hsm_version.h
index 5e782383..8b6c54bd 100644
--- a/common/hsm_version.h
+++ b/common/hsm_version.h
@@ -32,6 +32,7 @@
* v6 with hsm_passphrase changes: c646d557d7561dd885df3cad5b99c82895cda4b040699f3853980ec61b2873fa
* v6 with hsm_secret struct cleanup: 06c56396fe42f4f47911d7f865dd0004d264fc1348f89547743755b6b33fec90
* v6 with hsm_secret_type TLV: 7bb5deb2367482feb084d304ee14b2373d42910ad56484fbf47614dbb3d4cb74
+ * v6 with bip86_base in TLV: 6bb6e6ee256f22a6fb41856c90feebde3065a9074e79a46731e453a932be83f0
*/
#define HSM_MIN_VERSION 5
#define HSM_MAX_VERSION 6
diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c
index 7266b4f1..708fa84b 100644
--- a/hsmd/hsmd.c
+++ b/hsmd/hsmd.c
@@ -680,34 +680,6 @@ void hsmd_status_failed(enum status_failreason reason, const char *fmt, ...)
status_send_fatal(take(towire_status_fail(NULL, reason, str)));
}
-/* Handle BIP86 key derivation request */
-static struct io_plan *handle_derive_bip86_key(struct io_conn *conn,
- struct client *c,
- const u8 *msg_in)
-{
- u8 *reply;
- u32 index;
- bool is_change;
-
- /* Extract parameters from the wire message */
- if (!fromwire_hsmd_derive_bip86_key(msg_in, &index, &is_change))
- return bad_req(conn, c, msg_in);
-
- /* Check if we have a mnemonic-based HSM secret */
- if (!use_bip86_derivation(tal_bytelen(hsm_secret->secret_data))) {
- return bad_req_fmt(conn, c, msg_in,
- "BIP86 derivation requires mnemonic-based HSM secret");
- }
-
- /* Derive only the BIP86 base key (m/86'/0'/0') */
- struct ext_key bip86_base;
- derive_bip86_base_key(&bip86_base);
-
- /* Return the full BIP86 base extended key */
- reply = towire_hsmd_derive_bip86_key_reply(NULL, &bip86_base);
- return req_reply(conn, c, take(reply));
-}
-
/* Handle BIP86 pubkey check request */
static struct io_plan *handle_check_bip86_pubkey(struct io_conn *conn,
struct client *c,
@@ -768,8 +740,6 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
case WIRE_HSMD_CLIENT_HSMFD:
return pass_client_hsmfd(conn, c, c->msg_in);
- case WIRE_HSMD_DERIVE_BIP86_KEY:
- return handle_derive_bip86_key(conn, c, c->msg_in);
case WIRE_HSMD_CHECK_BIP86_PUBKEY:
return handle_check_bip86_pubkey(conn, c, c->msg_in);
@@ -864,7 +834,6 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
case WIRE_HSMD_PREAPPROVE_INVOICE_CHECK_REPLY:
case WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK_REPLY:
case WIRE_HSMD_CHECK_PUBKEY_REPLY:
- case WIRE_HSMD_DERIVE_BIP86_KEY_REPLY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY_REPLY:
case WIRE_HSMD_SIGN_ANCHORSPEND_REPLY:
case WIRE_HSMD_SIGN_HTLC_TX_MINGLE_REPLY:
diff --git a/hsmd/hsmd_wire.csv b/hsmd/hsmd_wire.csv
index 6472147f..7336feef 100644
--- a/hsmd/hsmd_wire.csv
+++ b/hsmd/hsmd_wire.csv
@@ -34,6 +34,7 @@ tlvtype,hsmd_init_tlvs,hsm_passphrase,1
tlvdata,hsmd_init_tlvs,hsm_passphrase,passphrase,wirestring,
#include <common/bip32.h>
+#include <wally_bip32.h>
# Sorry: I should have put version in v2 :(
msgtype,hsmd_init_reply_v4,114
# This gets upgraded when the wire protocol changes in incompatible
@@ -50,6 +51,8 @@ msgdata,hsmd_init_reply_v4,tlvs,hsmd_init_reply_v4_tlvs,
# TLV to indicate HSM secret type
tlvtype,hsmd_init_reply_v4_tlvs,hsm_secret_type,1
tlvdata,hsmd_init_reply_v4_tlvs,hsm_secret_type,hsm_type,u8,
+tlvtype,hsmd_init_reply_v4_tlvs,bip86_base,2
+tlvdata,hsmd_init_reply_v4_tlvs,bip86_base,base,ext_key,
# HSM initialization failure response
msgtype,hsmd_init_reply_failure,115
@@ -200,14 +203,6 @@ msgdata,hsmd_preapprove_keysend_check,check_only,bool,
msgtype,hsmd_preapprove_keysend_check_reply,152
msgdata,hsmd_preapprove_keysend_check_reply,approved,bool,
-# Derive BIP86 key using path m/86'/0'/0'/change/address_index
-msgtype,hsmd_derive_bip86_key,54
-msgdata,hsmd_derive_bip86_key,index,u32,
-msgdata,hsmd_derive_bip86_key,is_change,bool,
-
-msgtype,hsmd_derive_bip86_key_reply,154
-msgdata,hsmd_derive_bip86_key_reply,bip86_base,ext_key,
-
# Give me ECDH(node-id-secret,point)
msgtype,hsmd_ecdh_req,1
msgdata,hsmd_ecdh_req,point,pubkey,
diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c
index b362c812..5488302e 100644
--- a/hsmd/libhsmd.c
+++ b/hsmd/libhsmd.c
@@ -138,7 +138,6 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client,
case WIRE_HSMD_LOCK_OUTPOINT:
return (client->capabilities & HSM_PERM_LOCK_OUTPOINT) != 0;
- case WIRE_HSMD_DERIVE_BIP86_KEY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY:
case WIRE_HSMD_INIT:
case WIRE_HSMD_DEV_PREINIT:
@@ -209,7 +208,6 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client,
case WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK_REPLY:
case WIRE_HSMD_DERIVE_SECRET_REPLY:
case WIRE_HSMD_CHECK_PUBKEY_REPLY:
- case WIRE_HSMD_DERIVE_BIP86_KEY_REPLY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY_REPLY:
case WIRE_HSMD_SIGN_ANCHORSPEND_REPLY:
case WIRE_HSMD_SIGN_HTLC_TX_MINGLE_REPLY:
@@ -542,26 +540,9 @@ 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 {
- /* 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(tal_bytelen(secretstuff.bip32_seed))) {
- /* 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) {
+ /* Modern HSMs use bip86 for p2tr. */
+ if (is_p2tr(utxo->scriptPubkey, tal_bytelen(utxo->scriptPubkey), NULL)
+ && use_bip86_derivation(tal_bytelen(secretstuff.bip32_seed))) {
/* Use BIP86 derivation */
bip86_key(privkey, pubkey, utxo->keyindex);
} else {
@@ -2316,7 +2297,6 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client,
return handle_derive_secret(client, msg);
case WIRE_HSMD_CHECK_PUBKEY:
return handle_check_pubkey(client, msg);
- case WIRE_HSMD_DERIVE_BIP86_KEY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY:
/* This should be handled by hsmd.c, not libhsmd */
return hsmd_status_bad_request_fmt(
@@ -2373,7 +2353,6 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client,
case WIRE_HSMD_PREAPPROVE_KEYSEND_REPLY:
case WIRE_HSMD_PREAPPROVE_INVOICE_CHECK_REPLY:
case WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK_REPLY:
- case WIRE_HSMD_DERIVE_BIP86_KEY_REPLY:
case WIRE_HSMD_CHECK_PUBKEY_REPLY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY_REPLY:
case WIRE_HSMD_SIGN_ANCHORSPEND_REPLY:
@@ -2610,8 +2589,13 @@ u8 *hsmd_init(const u8 *secret_data, size_t secret_len, const u64 hsmd_version,
*/
/* Create TLV with HSM secret type */
struct tlv_hsmd_init_reply_v4_tlvs *tlvs = tlv_hsmd_init_reply_v4_tlvs_new(tmpctx);
- tlvs->hsm_secret_type = tal(tlvs, u8);
- *tlvs->hsm_secret_type = hsm_secret_type;
+ tlvs->hsm_secret_type = tal_dup(tlvs, u8, &hsm_secret_type);
+
+ /* If we have a mnemonic-based HSM, include the BIP86 base key */
+ if (use_bip86_derivation(tal_bytelen(secretstuff.bip32_seed))) {
+ tlvs->bip86_base = tal(tlvs, struct ext_key);
+ derive_bip86_base_key(tlvs->bip86_base);
+ }
return take(towire_hsmd_init_reply_v4(
NULL, hsmd_version, caps,
diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c
index 182cbe0d..1f0a2192 100644
--- a/lightningd/hsm_control.c
+++ b/lightningd/hsm_control.c
@@ -190,25 +190,9 @@ struct ext_key *hsm_init(struct lightningd *ld)
}
/* Check if we have a mnemonic-based HSM secret from TLV */
- bool is_mnemonic_secret = false;
- if (tlvs && tlvs->hsm_secret_type) {
- u8 secret_type = *tlvs->hsm_secret_type;
- is_mnemonic_secret = (secret_type == 2 || secret_type == 3); /* HSM_SECRET_MNEMONIC_NO_PASS or HSM_SECRET_MNEMONIC_WITH_PASS */
- }
-
- if (is_mnemonic_secret) {
- /* Try to get BIP86 base key from HSM (works only for mnemonic secrets) */
- ld->bip86_base = tal(ld, struct ext_key);
- msg = towire_hsmd_derive_bip86_key(NULL, 0, false);
- const u8 *reply = hsm_sync_req(tmpctx, ld, take(msg));
- if (fromwire_hsmd_derive_bip86_key_reply(reply, ld->bip86_base)) {
- /* BIP86 derivation succeeded */
- log_info(ld->log, "Using BIP86 for new addresses, BIP32 for channels (mnemonic HSM secret)");
- /* Keep bip32_base for channel operations, database, etc. */
- } else {
- /* BIP86 derivation failed unexpectedly */
- ld->bip86_base = tal_free(ld->bip86_base);
- }
+ if (tlvs->bip86_base) {
+ ld->bip86_base = tal_steal(ld, tlvs->bip86_base);
+ log_info(ld->log, "Using BIP86 for new addresses, BIP32 for channels (mnemonic HSM secret)");
} else {
/* Legacy HSM secret - don't attempt BIP86 derivation */
log_info(ld->log, "Using BIP32 derivation for all operations (legacy HSM secret)");
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index a91ec6db..2c5454b3 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -796,14 +796,6 @@ u8 *towire_hsmd_get_channel_basepoints(const tal_t *ctx UNNEEDED, const struct n
{
return NULL;
}
-u8 *towire_hsmd_derive_bip86_key(const tal_t *ctx UNNEEDED, u32 index UNNEEDED, bool is_change UNNEEDED)
-{
- return NULL;
-}
-bool fromwire_hsmd_derive_bip86_key_reply(const void *p UNNEEDED, struct ext_key *bip86_base UNNEEDED)
-{
- return true;
-}
bool wire_sync_write(int fd UNNEEDED, const void *msg TAKES UNNEEDED)
{
return true;
Why this scored 29/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.