hsmd: use the new mnemonic-compatible hsm_secret routines.

Changelog-Changed: hsmd: New nodes will now be created with a BIP-39 12-word phrase as their root secret.
Changelog-Deprecated: config: `encrypted-hsm` to require a passphrase (use `hsm-passphrase`).
Changelog-Added: config: `hsm-passphrase` indicates we should use a manual passphrase with the hsm secret.
This commit is contained in:
Sangbida Chaudhuri
2025-10-24 13:57:44 +10:30
committed by Rusty Russell
parent e3fe739f64
commit 218dc2fe20
11 changed files with 315 additions and 216 deletions

View File

@@ -12,9 +12,10 @@
#include <ccan/io/fdpass/fdpass.h>
#include <ccan/noerr/noerr.h>
#include <ccan/read_write_all/read_write_all.h>
#include <ccan/tal/grab_file/grab_file.h>
#include <ccan/tal/str/str.h>
#include <common/daemon_conn.h>
#include <common/hsm_encryption.h>
#include <common/hsm_secret.h>
#include <common/memleak.h>
#include <common/status.h>
#include <common/status_wiregen.h>
@@ -25,7 +26,9 @@
/*~ _wiregen files are autogenerated by tools/generate-wire.py */
#include <hsmd/libhsmd.h>
#include <hsmd/permissions.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <wally_bip39.h>
#include <wire/wire_io.h>
/*~ Each subdaemon is started with stdin connected to lightningd (for status
@@ -35,7 +38,7 @@
#define REQ_FD 3
/* Temporary storage for the secret until we pass it to `hsmd_init` */
struct secret hsm_secret;
struct hsm_secret hsm_secret;
/*~ We keep track of clients, but there's not much to keep. */
struct client {
@@ -270,33 +273,93 @@ static struct io_plan *req_reply(struct io_conn *conn,
return io_write_wire(conn, msg_out, client_read_next, c);
}
/*~ This encrypts the content of the `struct secret hsm_secret` and
* stores it in hsm_secret, this is called instead of create_hsm() if
* `lightningd` is started with --encrypted-hsm.
*/
static void create_encrypted_hsm(int fd, const struct secret *encryption_key)
/* Send an init reply failure message to lightningd and then call status_failed */
static void hsmd_send_init_reply_failure(enum hsm_secret_error error_code, enum status_failreason reason, const char *error_msg, ...)
{
struct encrypted_hsm_secret cipher;
u8 *msg;
va_list ap;
char *formatted_msg;
if (!encrypt_hsm_secret(encryption_key, &hsm_secret,
&cipher))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Encrypting hsm_secret");
if (!write_all(fd, cipher.data, ENCRYPTED_HSM_SECRET_LEN)) {
unlink_noerr("hsm_secret");
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Writing encrypted hsm_secret: %s", strerror(errno));
va_start(ap, error_msg);
formatted_msg = tal_vfmt(tmpctx, error_msg, ap);
va_end(ap);
/* Send the init reply failure first */
msg = towire_hsmd_init_reply_failure(NULL, error_code, formatted_msg);
if (msg) {
/* Send directly to lightningd via REQ_FD */
write_all(REQ_FD, msg, tal_bytelen(msg));
tal_free(msg);
}
/* Then call status_failed with the error message */
status_failed(reason, "%s", formatted_msg);
}
static void create_hsm(int fd)
static void create_hsm(int fd, const char *passphrase)
{
/*~ ccan/read_write_all has a more convenient return than write() where
* we'd have to check the return value == the length we gave: write()
* can return short on normal files if we run out of disk space. */
if (!write_all(fd, &hsm_secret, sizeof(hsm_secret))) {
/* ccan/noerr contains useful routines like this, which don't
* clobber errno, so we can use it in our error report. */
u8 *hsm_secret_data;
size_t hsm_secret_len;
int ret;
/* Always create a mnemonic-based hsm_secret */
u8 entropy[BIP39_ENTROPY_LEN_128];
char *mnemonic = NULL;
struct sha256 seed_hash;
/* Initialize wally tal context for libwally operations */
/* Generate random entropy for new mnemonic */
randombytes_buf(entropy, sizeof(entropy));
/* Generate mnemonic from entropy */
tal_wally_start();
ret = bip39_mnemonic_from_bytes(NULL, entropy, sizeof(entropy), &mnemonic);
tal_wally_end(tmpctx);
if (ret != WALLY_OK) {
unlink_noerr("hsm_secret");
hsmd_send_init_reply_failure(HSM_SECRET_ERR_SEED_DERIVATION_FAILED, STATUS_FAIL_INTERNAL_ERROR,
"Failed to generate mnemonic from entropy");
}
if (!mnemonic) {
unlink_noerr("hsm_secret");
hsmd_send_init_reply_failure(HSM_SECRET_ERR_SEED_DERIVATION_FAILED, STATUS_FAIL_INTERNAL_ERROR,
"Failed to get generated mnemonic");
}
/* Derive seed hash from mnemonic + passphrase (or zero if no passphrase) */
if (!derive_seed_hash(mnemonic, passphrase, &seed_hash)) {
unlink_noerr("hsm_secret");
hsmd_send_init_reply_failure(HSM_SECRET_ERR_SEED_DERIVATION_FAILED, STATUS_FAIL_INTERNAL_ERROR,
"Failed to derive seed hash from mnemonic");
}
/* Create hsm_secret format: seed_hash (32 bytes) + mnemonic */
hsm_secret_data = tal_arr(tmpctx, u8, 0);
towire_sha256(&hsm_secret_data, &seed_hash);
towire(&hsm_secret_data, mnemonic, strlen(mnemonic));
hsm_secret_len = tal_count(hsm_secret_data);
/* Derive the actual secret from mnemonic + passphrase for our global hsm_secret */
u8 bip32_seed[BIP39_SEED_LEN_512];
size_t bip32_seed_len;
tal_wally_start();
ret = bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len);
tal_wally_end(tmpctx);
if (ret != WALLY_OK) {
unlink_noerr("hsm_secret");
hsmd_send_init_reply_failure(HSM_SECRET_ERR_SEED_DERIVATION_FAILED, STATUS_FAIL_INTERNAL_ERROR,
"Failed to derive seed from mnemonic");
}
/* Use first 32 bytes for hsm_secret */
memcpy(&hsm_secret.secret, bip32_seed, sizeof(hsm_secret.secret));
/* Write the hsm_secret data to file */
if (!write_all(fd, hsm_secret_data, hsm_secret_len)) {
unlink_noerr("hsm_secret");
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"writing: %s", strerror(errno));
@@ -304,9 +367,12 @@ static void create_hsm(int fd)
}
/*~ We store our root secret in a "hsm_secret" file (like all of Core Lightning,
* we run in the user's .lightning directory). */
static void maybe_create_new_hsm(const struct secret *encryption_key,
bool random_hsm)
* we run in the user's .lightning directory).
*
* NOTE: This function no longer creates encrypted 32-byte secrets. New hsm_secret
* files will use mnemonic format with passphrases.
*/
static void maybe_create_new_hsm(const char *passphrase)
{
/*~ Note that this is opened for write-only, even though the permissions
* are set to read-only. That's perfectly valid! */
@@ -319,17 +385,10 @@ static void maybe_create_new_hsm(const struct secret *encryption_key,
"creating: %s", strerror(errno));
}
/*~ This is libsodium's cryptographic randomness routine: we assume
* it's doing a good job. */
if (random_hsm)
randombytes_buf(&hsm_secret, sizeof(hsm_secret));
/*~ If an encryption_key was provided, store an encrypted seed. */
if (encryption_key)
create_encrypted_hsm(fd, encryption_key);
/*~ Otherwise store the seed in clear.. */
else
create_hsm(fd);
/*~ Store the seed in clear. New hsm_secret files will use mnemonic format
* with passphrases, not encrypted 32-byte secrets. */
create_hsm(fd, passphrase);
/*~ fsync (mostly!) ensures that the file has reached the disk. */
if (fsync(fd) != 0) {
unlink_noerr("hsm_secret");
@@ -367,62 +426,35 @@ static void maybe_create_new_hsm(const struct secret *encryption_key,
/*~ We always load the HSM file, even if we just created it above. This
* both unifies the code paths, and provides a nice sanity check that the
* file contents are as they will be for future invocations. */
static void load_hsm(const struct secret *encryption_key)
static void load_hsm(const char *passphrase)
{
struct stat st;
int fd = open("hsm_secret", O_RDONLY);
if (fd < 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"opening: %s", strerror(errno));
if (stat("hsm_secret", &st) != 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"stating: %s", strerror(errno));
u8 *hsm_secret_contents;
struct hsm_secret *hsms;
enum hsm_secret_error err;
/* If the seed is stored in clear. */
if (st.st_size == 32) {
if (!read_all(fd, &hsm_secret, sizeof(hsm_secret)))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"reading: %s", strerror(errno));
/* If an encryption key was passed with a not yet encrypted hsm_secret,
* remove the old one and create an encrypted one. */
if (encryption_key) {
if (close(fd) != 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"closing: %s", strerror(errno));
if (remove("hsm_secret") != 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"removing clear hsm_secret: %s", strerror(errno));
maybe_create_new_hsm(encryption_key, false);
fd = open("hsm_secret", O_RDONLY);
if (fd < 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"opening: %s", strerror(errno));
}
/* Read the hsm_secret file */
size_t hsm_secret_len;
hsm_secret_contents = grab_file_contents(tmpctx, "hsm_secret", &hsm_secret_len);
if (!hsm_secret_contents) {
hsmd_send_init_reply_failure(HSM_SECRET_ERR_INVALID_FORMAT, STATUS_FAIL_INTERNAL_ERROR,
"Could not read hsm_secret: %s", strerror(errno));
}
/* If an encryption key was passed and the `hsm_secret` is stored
* encrypted, recover the seed from the cipher. */
else if (st.st_size == ENCRYPTED_HSM_SECRET_LEN) {
struct encrypted_hsm_secret encrypted_secret;
/* hsm_control must have checked it! */
assert(encryption_key);
if (!read_all(fd, encrypted_secret.data, ENCRYPTED_HSM_SECRET_LEN))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Reading encrypted hsm_secret: %s", strerror(errno));
if (!decrypt_hsm_secret(encryption_key, &encrypted_secret,
&hsm_secret)) {
/* Exit but don't throw a backtrace when the user made a mistake in typing
* its password. Instead exit and `lightningd` will be able to give
* an error message. */
exit(1);
}
/* Extract the secret using the new hsm_secret module */
tal_wally_start();
hsms = extract_hsm_secret(tmpctx, hsm_secret_contents,
hsm_secret_len,
passphrase, &err);
tal_wally_end(tmpctx);
if (!hsms) {
hsmd_send_init_reply_failure(err, STATUS_FAIL_INTERNAL_ERROR,
"Failed to load hsm_secret: %s", hsm_secret_error_str(err));
}
else
status_failed(STATUS_FAIL_INTERNAL_ERROR, "Invalid hsm_secret, "
"no plaintext nor encrypted"
" seed.");
close(fd);
/* Copy only the secret field to our global hsm_secret */
hsm_secret.secret = hsms->secret;
hsm_secret.type = hsms->type;
hsm_secret.mnemonic = hsms->mnemonic;
}
/*~ We have a pre-init call in developer mode, to set dev flags */
@@ -458,6 +490,7 @@ static struct io_plan *init_hsm(struct io_conn *conn,
const u8 *msg_in)
{
struct secret *hsm_encryption_key;
const char *hsm_passphrase = NULL;
struct bip32_key_version bip32_key_version;
u32 minversion, maxversion;
const u32 our_minversion = 4, our_maxversion = 6;
@@ -494,27 +527,13 @@ static struct io_plan *init_hsm(struct io_conn *conn,
* think this is some kind of obscure CLN hazing ritual? Anyway, the
* passphrase needs to be *appended* to the mnemonic, so the HSM needs
* the raw passphrase. To avoid a compatibility break, I put it inside
* the TLV, and left the old "hsm_encryption_key" field in place, even
* though we override it here for the old-style non-BIP39 hsm_secret. */
if (tlvs->hsm_passphrase) {
const char *hsm_passphrase = (const char *)tlvs->hsm_passphrase;
const char *err_msg;
* the TLV, and left the old "hsm_encryption_key" field in place (and lightningd
* never sets that anymore), and we use the TLV instead. */
if (tlvs->hsm_passphrase)
hsm_passphrase = tlvs->hsm_passphrase;
hsm_encryption_key = tal(NULL, struct secret);
if (hsm_secret_encryption_key_with_exitcode(hsm_passphrase, hsm_encryption_key, &err_msg) != 0)
return bad_req_fmt(conn, c, msg_in,
"Bad passphrase: %s", err_msg);
}
tal_free(tlvs);
/*~ The memory is actually copied in towire(), so lock the `hsm_secret`
* encryption key (new) memory again here. */
if (hsm_encryption_key && sodium_mlock(hsm_encryption_key,
sizeof(hsm_encryption_key)) != 0)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Could not lock memory for hsm_secret encryption key.");
/*~ Don't swap this. */
sodium_mlock(hsm_secret.data, sizeof(hsm_secret.data));
sodium_mlock(hsm_secret.secret.data, sizeof(hsm_secret.secret.data));
if (!developer) {
assert(!dev_force_privkey);
@@ -526,16 +545,15 @@ static struct io_plan *init_hsm(struct io_conn *conn,
/* Once we have read the init message we know which params the master
* will use */
c->chainparams = chainparams;
maybe_create_new_hsm(hsm_encryption_key, true);
load_hsm(hsm_encryption_key);
/*~ We don't need the hsm_secret encryption key anymore. */
if (hsm_encryption_key)
discard_key(take(hsm_encryption_key));
maybe_create_new_hsm(hsm_passphrase);
load_hsm(hsm_passphrase);
/* Define the minimum common max version for the hsmd one */
hsmd_mutual_version = maxversion < our_maxversion ? maxversion : our_maxversion;
return req_reply(conn, c, hsmd_init(hsm_secret, hsmd_mutual_version,
/* This was tallocated off NULL, and memleak complains if we don't free it */
tal_free(tlvs);
return req_reply(conn, c, hsmd_init(hsm_secret.secret, hsmd_mutual_version,
bip32_key_version));
}
@@ -756,6 +774,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c)
case WIRE_HSMD_SIGN_WITHDRAWAL_REPLY:
case WIRE_HSMD_SIGN_INVOICE_REPLY:
case WIRE_HSMD_INIT_REPLY_V4:
case WIRE_HSMD_INIT_REPLY_FAILURE:
case WIRE_HSMD_DERIVE_SECRET_REPLY:
case WIRE_HSMSTATUS_CLIENT_BAD_REQUEST:
case WIRE_HSMD_SIGN_COMMITMENT_TX_REPLY:

View File

@@ -47,6 +47,11 @@ msgdata,hsmd_init_reply_v4,node_id,node_id,
msgdata,hsmd_init_reply_v4,bip32,ext_key,
msgdata,hsmd_init_reply_v4,bolt12,pubkey,
# HSM initialization failure response
msgtype,hsmd_init_reply_failure,115
msgdata,hsmd_init_reply_failure,error_code,u32,
msgdata,hsmd_init_reply_failure,error_message,wirestring,
# Declare a new channel.
msgtype,hsmd_new_channel,30
msgdata,hsmd_new_channel,id,node_id,
1 # Clients should not give a bad request but not the HSM's decision to crash.
47 msgtype,hsmd_new_channel,30 msgtype,hsmd_init_reply_failure,115
48 msgdata,hsmd_new_channel,id,node_id, msgdata,hsmd_init_reply_failure,error_code,u32,
49 msgdata,hsmd_new_channel,dbid,u64, msgdata,hsmd_init_reply_failure,error_message,wirestring,
50 # Declare a new channel.
51 msgtype,hsmd_new_channel,30
52 msgdata,hsmd_new_channel,id,node_id,
53 msgdata,hsmd_new_channel,dbid,u64,
54 # No value returned.
55 # No value returned. msgtype,hsmd_new_channel_reply,130
56 msgtype,hsmd_new_channel_reply,130 # Get a new HSM FD, with the specified permissions
57 # Get a new HSM FD, with the specified permissions msgtype,hsmd_client_hsmfd,9

View File

@@ -172,6 +172,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client,
case WIRE_HSMD_SIGN_WITHDRAWAL_REPLY:
case WIRE_HSMD_SIGN_INVOICE_REPLY:
case WIRE_HSMD_INIT_REPLY_V4:
case WIRE_HSMD_INIT_REPLY_FAILURE:
case WIRE_HSMSTATUS_CLIENT_BAD_REQUEST:
case WIRE_HSMD_SIGN_COMMITMENT_TX_REPLY:
case WIRE_HSMD_VALIDATE_COMMITMENT_TX_REPLY:
@@ -2303,6 +2304,7 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client,
case WIRE_HSMD_SIGN_WITHDRAWAL_REPLY:
case WIRE_HSMD_SIGN_INVOICE_REPLY:
case WIRE_HSMD_INIT_REPLY_V4:
case WIRE_HSMD_INIT_REPLY_FAILURE:
case WIRE_HSMSTATUS_CLIENT_BAD_REQUEST:
case WIRE_HSMD_SIGN_COMMITMENT_TX_REPLY:
case WIRE_HSMD_VALIDATE_COMMITMENT_TX_REPLY: