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:
committed by
Rusty Russell
parent
e3fe739f64
commit
218dc2fe20
245
hsmd/hsmd.c
245
hsmd/hsmd.c
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user