hsmd/libhsmd: add BIP86 base-key
What changed, and why it matters
This commit adds support for deriving Bitcoin BIP86-style keys inside Core Lightning's Hardware Security Module (HSM) daemon. It introduces two new wire messages that let authorized clients ask the HSM to derive a BIP86 base key and to check whether a public key matches the HSM's own derivation. The commit also adds a new permission bit controlling access to these messages. There is no direct evidence in the commit that this fixes an active vulnerability; it reads as feature work to support a new wallet/key derivation path, with a sanity-check helper to catch accidental derivation mismatches.
Review the new HSM_PERM_DERIVE_BIP86_KEY permission assignments to ensure only trusted clients receive it. Verify that the BIP86 base path (m/86'/0'/0') and final derivation path (m/86'/0'/0'/0/index) match the intended BIP86 specification and network conventions. Confirm that rejecting legacy 32-byte HSM secrets is acceptable for deployments. Treat as feature code, not an urgent security patch, unless additional context emerges.
Security signals we found
New HSM capability/permission bit added (HSM_PERM_DERIVE_BIP86_KEY)
New wire messages guarded by capability check in hsmd_check_client_capabilities
HSM aborts on derivation mismatch (status_failed STATUS_FAIL_INTERNAL_ERROR)
Requires 64-byte mnemonic-based BIP32 seed; rejects legacy 32-byte secrets
BIP86 derivation path uses hardcoded m/86'/0'/0' base and non-hardened 0/index final path
No explicit security relevance, CVE, or bug-fix framing in commit message
Evidence from the diff
The patch moves BIP86 key derivation helpers into libhsmd.c and wires them up in hsmd.c. New handlers handle_derive_bip86_key() and handle_check_bip86_pubkey() are added, guarded by a new HSM_PERM_DERIVE_BIP86_KEY capability. derive_bip86_base_key() derives m/86’/0’/0’ from the full 64-byte BIP32 seed, and bip86_key() derives m/86’/0’/0’/0/index. The check handler aborts the HSM via status_failed() if the supplied pubkey does not match the HSM-derived one. The code rejects legacy non-mnemonic HSM secrets and rejects hardened indices >= 0x80000000 for the final non-hardened path. The commit message explicitly frames the pubkey check as a future sanity check against accidental bit flips, not as a security fix.
Changed components
hsmd/hsmd.chsmd/libhsmd.chsmd/libhsmd.hhsmd/permissions.hInspect captured patch +151 / −6
diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c
index 63cbc0f..616d7fe 100644
--- a/hsmd/hsmd.c
+++ b/hsmd/hsmd.c
@@ -28,6 +28,7 @@
#include <hsmd/permissions.h>
#include <stdarg.h>
#include <sys/stat.h>
+#include <wally_bip32.h>
#include <wally_bip39.h>
#include <wire/wire_io.h>
@@ -681,6 +682,68 @@ 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 (hsm_secret_size(&hsm_secret) != 64) {
+ 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,
+ const u8 *msg_in)
+{
+ u32 index;
+ struct pubkey their_pubkey, our_pubkey;
+ struct privkey our_privkey;
+ u8 *reply;
+
+ if (!fromwire_hsmd_check_bip86_pubkey(msg_in, &index, &their_pubkey))
+ return bad_req(conn, c, msg_in);
+
+ /* Check if we have a mnemonic-based HSM secret */
+ if (hsm_secret_size(&hsm_secret) != 64) {
+ return bad_req_fmt(conn, c, msg_in,
+ "BIP86 derivation requires mnemonic-based HSM secret");
+ }
+
+ /* We abort if lightningd asks for a stupid index. */
+ bip86_key(&our_privkey, &our_pubkey, index);
+ if (!pubkey_eq(&our_pubkey, &their_pubkey)) {
+ status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "BIP86 derivation index %u differed:"
+ " they got %s, we got %s",
+ index,
+ fmt_pubkey(tmpctx, &their_pubkey),
+ fmt_pubkey(tmpctx, &our_pubkey));
+ }
+
+ reply = towire_hsmd_check_bip86_pubkey_reply(NULL, true);
+ return req_reply(conn, c, take(reply));
+}
+
/*~ This is the core of the HSM daemon: handling requests. */
static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
{
@@ -711,6 +774,10 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
if (developer)
return handle_memleak(conn, c, c->msg_in);
/* fall thru */
+ 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);
case WIRE_HSMD_NEW_CHANNEL:
case WIRE_HSMD_SETUP_CHANNEL:
case WIRE_HSMD_CHECK_OUTPOINT:
@@ -750,8 +817,6 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
case WIRE_HSMD_SIGN_REMOTE_HTLC_TO_US:
case WIRE_HSMD_SIGN_DELAYED_PAYMENT_TO_US:
case WIRE_HSMD_CHECK_PUBKEY:
- case WIRE_HSMD_DERIVE_BIP86_KEY:
- case WIRE_HSMD_CHECK_BIP86_PUBKEY:
case WIRE_HSMD_SIGN_ANY_PENALTY_TO_US:
case WIRE_HSMD_SIGN_ANY_DELAYED_PAYMENT_TO_US:
case WIRE_HSMD_SIGN_ANY_REMOTE_HTLC_TO_US:
diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c
index 51cb238..d4ee9ef 100644
--- a/hsmd/libhsmd.c
+++ b/hsmd/libhsmd.c
@@ -16,6 +16,8 @@
#include <secp256k1_ecdh.h>
#include <secp256k1_schnorrsig.h>
#include <sodium/utils.h>
+#include <wally_bip32.h>
+#include <wally_bip39.h>
#include <wally_psbt.h>
/* The negotiated protocol version ends up in here. */
@@ -45,6 +47,9 @@ struct {
/* Have we initialized the secretstuff? */
bool initialized = false;
+/* BIP32 key version for network compatibility */
+static struct bip32_key_version network_bip32_key_version;
+
/* Do we fail all preapprove requests? */
bool dev_fail_preapprove = false;
bool dev_no_preapprove_check = false;
@@ -133,6 +138,9 @@ 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:
+ return (client->capabilities & HSM_PERM_DERIVE_BIP86_KEY) != 0;
case WIRE_HSMD_INIT:
case WIRE_HSMD_DEV_PREINIT:
case WIRE_HSMD_NEW_CHANNEL:
@@ -154,8 +162,6 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client,
case WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK:
case WIRE_HSMD_DERIVE_SECRET:
case WIRE_HSMD_CHECK_PUBKEY:
- case WIRE_HSMD_DERIVE_BIP86_KEY:
- case WIRE_HSMD_CHECK_BIP86_PUBKEY:
case WIRE_HSMD_SIGN_ANY_PENALTY_TO_US:
case WIRE_HSMD_SIGN_ANY_DELAYED_PAYMENT_TO_US:
case WIRE_HSMD_SIGN_ANY_REMOTE_HTLC_TO_US:
@@ -2289,10 +2295,11 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client,
return handle_check_pubkey(client, msg);
case WIRE_HSMD_DERIVE_BIP86_KEY:
case WIRE_HSMD_CHECK_BIP86_PUBKEY:
- /* Not implemented yet */
+ /* This should be handled by hsmd.c, not libhsmd */
return hsmd_status_bad_request_fmt(
client, msg,
- "Message of type %s not implemented yet",
+ "Message of type %s should be handled externally to "
+ "libhsmd",
hsmd_wire_name(fromwire_peektype(msg)));
case WIRE_HSMD_SIGN_ANY_DELAYED_PAYMENT_TO_US:
return handle_sign_any_delayed_payment_to_us(client, msg);
@@ -2354,6 +2361,71 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client,
return hsmd_status_bad_request(client, msg, "Unknown request");
}
+/* BIP86 key derivation functions moved from hsmd.c */
+void derive_bip86_base_key(struct ext_key *bip86_base)
+{
+ /* Check if we have the full BIP32 seed available */
+ if (secretstuff.bip32_seed_len < BIP39_SEED_LEN_512) {
+ hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "BIP86 derivation requires full 64-byte BIP32 seed (not available in legacy format)");
+ }
+
+ /* First create the master key from the seed */
+ struct ext_key master_key;
+
+ if (bip32_key_from_seed(secretstuff.bip32_seed, secretstuff.bip32_seed_len, network_bip32_key_version.bip32_privkey_version, 0, &master_key) != WALLY_OK) {
+ hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "Failed to create master key from BIP32 seed");
+ }
+
+ /* Set up the BIP86 base path: m/86'/0'/0' */
+ u32 base_path[3];
+ base_path[0] = 86 | 0x80000000; /* 86' */
+ base_path[1] = 0x80000000; /* 0' */
+ base_path[2] = 0x80000000; /* 0' */
+
+ /* Derive the BIP86 base key */
+ if (bip32_key_from_parent_path(&master_key, base_path, 3, BIP32_FLAG_KEY_PRIVATE, bip86_base) != WALLY_OK) {
+ hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "Failed to derive BIP86 base key");
+ }
+}
+
+/*~ Get the BIP86 keys for this given index: if privkey is NULL, we
+ * don't fill it in. This derives the full path: m/86'/0'/0'/0/index */
+void bip86_key(struct privkey *privkey, struct pubkey *pubkey, u32 index)
+{
+ struct privkey unused_priv;
+
+ if (privkey == NULL)
+ privkey = &unused_priv;
+
+ if (index >= BIP32_INITIAL_HARDENED_CHILD)
+ hsmd_status_failed(STATUS_FAIL_MASTER_IO, "Index %u too great", index);
+
+ /* Derive the BIP86 base key using the helper function */
+ struct ext_key bip86_base;
+ derive_bip86_base_key(&bip86_base);
+
+ /* Now derive the specific index: m/86'/0'/0'/0/index */
+ u32 final_path[2];
+ final_path[0] = 0; /* change (0 for receive) */
+ final_path[1] = index; /* address_index */
+
+ struct ext_key final_key;
+ if (bip32_key_from_parent_path(&bip86_base, final_path, 2, BIP32_FLAG_KEY_PRIVATE, &final_key) != WALLY_OK) {
+ hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "BIP86 derivation of index %u failed", index);
+ }
+
+ /* Convert to our format */
+ memcpy(privkey->secret.data, final_key.priv_key+1, 32);
+ if (!secp256k1_ec_pubkey_create(secp256k1_ctx, &pubkey->pubkey,
+ privkey->secret.data))
+ hsmd_status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "BIP86 pubkey %u create failed", index);
+}
+
u8 *hsmd_init(const u8 *secret_data, size_t secret_len, const u64 hsmd_version,
struct bip32_key_version bip32_key_version)
{
@@ -2377,6 +2449,9 @@ u8 *hsmd_init(const u8 *secret_data, size_t secret_len, const u64 hsmd_version,
};
u32 *caps;
+ /*~ Store the BIP32 key version for network compatibility */
+ network_bip32_key_version = bip32_key_version;
+
/* new: keep the full 32/64B root */
secretstuff.bip32_seed_len = secret_len;
secretstuff.bip32_seed = notleak(tal_dup_arr(NULL, u8, secret_data, secret_len, 0));
diff --git a/hsmd/libhsmd.h b/hsmd/libhsmd.h
index 35d6904..3ebb3eb 100644
--- a/hsmd/libhsmd.h
+++ b/hsmd/libhsmd.h
@@ -88,6 +88,10 @@ void hsmd_status_failed(enum status_failreason code,
bool hsmd_check_client_capabilities(struct hsmd_client *client,
enum hsmd_wire t);
+/* BIP86 key derivation functions */
+void derive_bip86_base_key(struct ext_key *bip86_base);
+void bip86_key(struct privkey *privkey, struct pubkey *pubkey, u32 index);
+
/* The negotiated protocol version ends up in here. */
extern u64 hsmd_mutual_version;
diff --git a/hsmd/permissions.h b/hsmd/permissions.h
index 9f1bf45..9cc63a8 100644
--- a/hsmd/permissions.h
+++ b/hsmd/permissions.h
@@ -11,6 +11,7 @@
#define HSM_PERM_SIGN_WILL_FUND_OFFER 64
#define HSM_PERM_SIGN_SPLICE_TX 128
#define HSM_PERM_LOCK_OUTPOINT 256
+#define HSM_PERM_DERIVE_BIP86_KEY 512
#define HSM_PERM_MASTER 1024
#endif /* LIGHTNING_HSMD_PERMISSIONS_H */
Why this scored 33/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.