diff --git a/common/Makefile b/common/Makefile index dec98e099b26..ea59672fece9 100644 --- a/common/Makefile +++ b/common/Makefile @@ -46,6 +46,7 @@ COMMON_SRC_NOGEN := \ common/hmac.c \ common/hsm_capable.c \ common/hsm_encryption.c \ + common/hsm_secret.c \ common/htlc_state.c \ common/htlc_trim.c \ common/htlc_tx.c \ diff --git a/common/hsm_secret.c b/common/hsm_secret.c new file mode 100644 index 000000000000..d8c3031e2746 --- /dev/null +++ b/common/hsm_secret.c @@ -0,0 +1,508 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Length of the encrypted hsm secret header. */ +#define HS_HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES +/* From libsodium: "The ciphertext length is guaranteed to always be message + * length + ABYTES" */ +#define HS_CIPHERTEXT_LEN \ + (sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES) +/* Total length of an encrypted hsm_secret */ +#define ENCRYPTED_HSM_SECRET_LEN (HS_HEADER_LEN + HS_CIPHERTEXT_LEN) + +static void destroy_secret(struct secret *secret) +{ + sodium_munlock(secret->data, sizeof(secret->data)); +} + +struct secret *get_encryption_key(const tal_t *ctx, const char *passphrase) +{ + struct secret *secret = tal(ctx, struct secret); + const u8 salt[16] = "c-lightning\0\0\0\0\0"; + + /* Check bounds. */ + if (strlen(passphrase) < crypto_pwhash_argon2id_PASSWD_MIN) { + return tal_free(secret); + } else if (strlen(passphrase) > crypto_pwhash_argon2id_PASSWD_MAX) { + return tal_free(secret); + } + + /* Don't swap the encryption key ! */ + if (sodium_mlock(secret->data, sizeof(secret->data)) != 0) + return tal_free(secret); + tal_add_destructor(secret, destroy_secret); + + /* Now derive the key. */ + if (crypto_pwhash(secret->data, sizeof(secret->data), passphrase, strlen(passphrase), salt, + /* INTERACTIVE needs 64 MiB of RAM, MODERATE needs 256, + * and SENSITIVE needs 1024. */ + crypto_pwhash_argon2id_OPSLIMIT_MODERATE, + crypto_pwhash_argon2id_MEMLIMIT_MODERATE, + crypto_pwhash_ALG_ARGON2ID13) != 0) { + return tal_free(secret); + } + + return secret; +} + +bool hsm_secret_needs_passphrase(const u8 *hsm_secret, size_t len) +{ + enum hsm_secret_type type = detect_hsm_secret_type(hsm_secret, len); + + switch (type) { + case HSM_SECRET_ENCRYPTED: + case HSM_SECRET_MNEMONIC_WITH_PASS: + return true; + case HSM_SECRET_PLAIN: + case HSM_SECRET_MNEMONIC_NO_PASS: + case HSM_SECRET_INVALID: + return false; + } + abort(); +} + +enum hsm_secret_type detect_hsm_secret_type(const u8 *hsm_secret, size_t len) +{ + /* Check for invalid cases first and return early */ + if (len < HSM_SECRET_PLAIN_SIZE) + return HSM_SECRET_INVALID; + + /* Legacy 32-byte plain format */ + if (len == HSM_SECRET_PLAIN_SIZE) + return HSM_SECRET_PLAIN; + + /* Legacy 73-byte encrypted format */ + if (len == ENCRYPTED_HSM_SECRET_LEN) + return HSM_SECRET_ENCRYPTED; + + /* Check if it starts with our type bytes (mnemonic formats) */ + if (memeqzero(hsm_secret, 32)) + return HSM_SECRET_MNEMONIC_NO_PASS; + else + return HSM_SECRET_MNEMONIC_WITH_PASS; +} + +/* Helper function to derive seed hash from mnemonic + passphrase */ +bool derive_seed_hash(const char *mnemonic, const char *passphrase, struct sha256 *seed_hash) +{ + if (!passphrase) { + /* No passphrase - return zero hash */ + memset(seed_hash, 0, sizeof(*seed_hash)); + return true; + } + + u8 bip32_seed[BIP39_SEED_LEN_512]; + size_t bip32_seed_len; + + if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) + return false; + + sha256(seed_hash, bip32_seed, sizeof(bip32_seed)); + return true; +} + +/* Validate the passphrase for a mnemonic secret */ +bool validate_mnemonic_passphrase(const u8 *hsm_secret, size_t len, const char *passphrase) +{ + enum hsm_secret_type type = detect_hsm_secret_type(hsm_secret, len); + + if (type != HSM_SECRET_MNEMONIC_WITH_PASS) + return true; /* No validation needed */ + + /* First 32 bytes are the stored seed hash */ + const struct sha256 *stored_hash = (const struct sha256 *)hsm_secret; + struct sha256 computed_hash; + + /* Extract mnemonic portion (skip first 32 bytes which are seed hash) */ + const char *mnemonic_start = (const char *)(hsm_secret + sizeof(struct sha256)); + + if (!derive_seed_hash(mnemonic_start, passphrase, &computed_hash)) + return false; + + return sha256_eq(stored_hash, &computed_hash); +} + +static bool decrypt_hsm_secret(const struct secret *encryption_key, + const u8 *cipher, + struct secret *output) +{ + crypto_secretstream_xchacha20poly1305_state crypto_state; + + /* The header part */ + if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, cipher, + encryption_key->data) != 0) + return false; + /* The ciphertext part */ + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, output->data, + NULL, 0, + cipher + HS_HEADER_LEN, + HS_CIPHERTEXT_LEN, + NULL, 0) != 0) + return false; + + return true; +} + +/* Helper function to convert error codes to human-readable messages */ +const char *hsm_secret_error_str(enum hsm_secret_error err) +{ + switch (err) { + case HSM_SECRET_OK: + return "Success"; + case HSM_SECRET_ERR_PASSPHRASE_REQUIRED: + return "Passphrase required but not provided"; + case HSM_SECRET_ERR_PASSPHRASE_NOT_NEEDED: + return "Passphrase provided but not needed"; + case HSM_SECRET_ERR_WRONG_PASSPHRASE: + return "Wrong passphrase"; + case HSM_SECRET_ERR_INVALID_MNEMONIC: + return "Invalid mnemonic"; + case HSM_SECRET_ERR_ENCRYPTION_FAILED: + return "Encryption failed"; + case HSM_SECRET_ERR_WORDLIST_FAILED: + return "Could not load wordlist"; + case HSM_SECRET_ERR_SEED_DERIVATION_FAILED: + return "Could not derive seed from mnemonic"; + case HSM_SECRET_ERR_INVALID_FORMAT: + return "Invalid hsm_secret format"; + case HSM_SECRET_ERR_TERMINAL: + return "Terminal error"; + case HSM_SECRET_ERR_MEMORY: + return "Memory error"; + } + return "Unknown error"; +} + +static struct hsm_secret *extract_plain_secret(const tal_t *ctx, + const u8 *hsm_secret, + size_t len, + enum hsm_secret_error *err) +{ + struct hsm_secret *hsms = tal(ctx, struct hsm_secret); + + hsms->type = HSM_SECRET_PLAIN; + hsms->mnemonic = NULL; + memcpy(&hsms->secret, hsm_secret, sizeof(hsms->secret)); + + *err = HSM_SECRET_OK; + return hsms; +} + +static struct hsm_secret *extract_encrypted_secret(const tal_t *ctx, + const u8 *hsm_secret, + size_t len, + const char *passphrase, + enum hsm_secret_error *err) +{ + struct hsm_secret *hsms = tal(ctx, struct hsm_secret); + struct secret *encryption_key; + bool decrypt_success; + + if (!passphrase) { + *err = HSM_SECRET_ERR_PASSPHRASE_REQUIRED; + return tal_free(hsms); + } + encryption_key = get_encryption_key(tmpctx, passphrase); + if (!encryption_key) { + *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; + return tal_free(hsms); + } + + /* Clear secret data first in case of partial decryption */ + memset(&hsms->secret, 0, sizeof(hsms->secret)); + + /* Attempt decryption */ + decrypt_success = decrypt_hsm_secret(encryption_key, hsm_secret, &hsms->secret); + + /* Clear encryption key immediately after use */ + discard_key(encryption_key); + + if (!decrypt_success) { + /* Clear any partial decryption data */ + memset(&hsms->secret, 0, sizeof(hsms->secret)); + *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; + return tal_free(hsms); + } + + hsms->type = HSM_SECRET_ENCRYPTED; + hsms->mnemonic = NULL; + + *err = HSM_SECRET_OK; + return hsms; +} + +static struct hsm_secret *extract_mnemonic_secret(const tal_t *ctx, + const u8 *hsm_secret, + size_t len, + const char *passphrase, + enum hsm_secret_error *err) +{ + struct hsm_secret *hsms = tal(ctx, struct hsm_secret); + struct words *words; + const u8 *mnemonic_start; + size_t mnemonic_len; + enum hsm_secret_type type; + + type = detect_hsm_secret_type(hsm_secret, len); + hsms->type = type; + + /* Extract mnemonic portion (skip first 32 bytes which are passphrase hash) */ + mnemonic_start = hsm_secret + PASSPHRASE_HASH_LEN; + mnemonic_len = len - PASSPHRASE_HASH_LEN; + + /* Validate passphrase if required */ + if (type == HSM_SECRET_MNEMONIC_WITH_PASS) { + if (!passphrase) { + *err = HSM_SECRET_ERR_PASSPHRASE_REQUIRED; + return tal_free(hsms); + } + + if (!validate_mnemonic_passphrase(hsm_secret, len, passphrase)) { + *err = HSM_SECRET_ERR_WRONG_PASSPHRASE; + return tal_free(hsms); + } + } else { + if (passphrase) { + *err = HSM_SECRET_ERR_PASSPHRASE_NOT_NEEDED; + return tal_free(hsms); + } + } + + /* Copy and validate mnemonic */ + hsms->mnemonic = tal_strndup(hsms, (const char *)mnemonic_start, mnemonic_len); + + /* Load wordlist and validate mnemonic */ + if (bip39_get_wordlist("en", &words) != WALLY_OK) { + *err = HSM_SECRET_ERR_WORDLIST_FAILED; + return tal_free(hsms); + } + + if (bip39_mnemonic_validate(words, hsms->mnemonic) != WALLY_OK) { + *err = HSM_SECRET_ERR_INVALID_MNEMONIC; + return tal_free(hsms); + } + + /* Derive the seed from the mnemonic */ + u8 bip32_seed[BIP39_SEED_LEN_512]; + size_t bip32_seed_len; + const char *seed_passphrase = (type == HSM_SECRET_MNEMONIC_WITH_PASS) ? passphrase : NULL; + + if (bip39_mnemonic_to_seed(hsms->mnemonic, seed_passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) { + *err = HSM_SECRET_ERR_SEED_DERIVATION_FAILED; + return tal_free(hsms); + } + + /* We only use the first 32 bytes for the hsm_secret */ + memcpy(hsms->secret.data, bip32_seed, sizeof(hsms->secret.data)); + + *err = HSM_SECRET_OK; + return hsms; +} + +/* If hsm_secret_needs_passphrase, passphrase must not be NULL. + * Returns NULL on failure. */ +struct hsm_secret *extract_hsm_secret(const tal_t *ctx, + const u8 *hsm_secret, size_t len, + const char *passphrase, + enum hsm_secret_error *err) +{ + enum hsm_secret_type type = detect_hsm_secret_type(hsm_secret, len); + + switch (type) { + case HSM_SECRET_PLAIN: + return extract_plain_secret(ctx, hsm_secret, len, err); + case HSM_SECRET_ENCRYPTED: + return extract_encrypted_secret(ctx, hsm_secret, len, passphrase, err); + case HSM_SECRET_MNEMONIC_NO_PASS: + return extract_mnemonic_secret(ctx, hsm_secret, len, NULL, err); + case HSM_SECRET_MNEMONIC_WITH_PASS: + return extract_mnemonic_secret(ctx, hsm_secret, len, passphrase, err); + case HSM_SECRET_INVALID: + *err = HSM_SECRET_ERR_INVALID_FORMAT; + return NULL; + } +} + +bool encrypt_legacy_hsm_secret(const struct secret *encryption_key, + const struct secret *hsm_secret, + u8 *output) +{ + crypto_secretstream_xchacha20poly1305_state crypto_state; + + if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, output, + encryption_key->data) != 0) + return false; + if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, + output + HS_HEADER_LEN, + NULL, hsm_secret->data, + sizeof(hsm_secret->data), + /* Additional data and tag */ + NULL, 0, 0)) + return false; + + return true; +} + +/* Returns -1 on error (and sets errno), 0 if not encrypted, 1 if it is */ +int is_legacy_hsm_secret_encrypted(const char *path) +{ + struct stat st; + + if (stat(path, &st) != 0) + return -1; + + return st.st_size == ENCRYPTED_HSM_SECRET_LEN; +} + +void discard_key(struct secret *key TAKES) +{ + /* sodium_munlock() also zeroes the memory. */ + sodium_munlock(key->data, sizeof(key->data)); + if (taken(key)) + tal_free(key); +} + +static void destroy_passphrase(char *passphrase) +{ + sodium_memzero(passphrase, tal_bytelen(passphrase)); + sodium_munlock(passphrase, tal_bytelen(passphrase)); +} + +/* Disable terminal echo if needed */ +static bool disable_echo(struct termios *saved_term) +{ + if (!isatty(fileno(stdin))) + return false; + + if (tcgetattr(fileno(stdin), saved_term) != 0) + return false; + + struct termios tmp = *saved_term; + tmp.c_lflag &= ~ECHO; + + if (tcsetattr(fileno(stdin), TCSANOW, &tmp) != 0) + return false; + + return true; +} + +/* Restore terminal echo if it was disabled */ +static void restore_echo(const struct termios *saved_term) +{ + tcsetattr(fileno(stdin), TCSANOW, saved_term); +} + +/* Read line from stdin (uses malloc internally) */ +static char *read_line(void) +{ + char *line = NULL; + size_t size = 0; + + if (getline(&line, &size, stdin) < 0) { + free(line); + return NULL; + } + + /* Strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + return line; +} + +const char *read_stdin_pass(const tal_t *ctx, enum hsm_secret_error *err) +{ + *err = HSM_SECRET_OK; + + struct termios saved_term; + bool echo_disabled = disable_echo(&saved_term); + if (isatty(fileno(stdin)) && !echo_disabled) { + *err = HSM_SECRET_ERR_TERMINAL; + return NULL; + } + + char *input = read_line(); + if (!input) { + if (echo_disabled) + restore_echo(&saved_term); + *err = HSM_SECRET_ERR_INVALID_FORMAT; + return NULL; + } + + size_t len = strlen(input); + char *passphrase = tal_arr(ctx, char, len + 1); + strcpy(passphrase, input); + free(input); + + /* Memory locking is mandatory: failure means we're on an insecure system */ + if (sodium_mlock(passphrase, len + 1) != 0) + abort(); + + tal_add_destructor(passphrase, destroy_passphrase); + + if (echo_disabled) + restore_echo(&saved_term); + + return passphrase; +} + +/* Add this function to hsm_secret.c */ +const char *read_stdin_mnemonic(const tal_t *ctx, enum hsm_secret_error *err) +{ + *err = HSM_SECRET_OK; + + printf("Introduce your BIP39 word list separated by space (at least 12 words):\n"); + fflush(stdout); + + char *line = NULL; + size_t size = 0; + if (getline(&line, &size, stdin) < 0) { + free(line); + *err = HSM_SECRET_ERR_INVALID_FORMAT; + return NULL; + } + + /* Strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + /* Validate mnemonic */ + struct words *words; + if (bip39_get_wordlist("en", &words) != WALLY_OK) { + free(line); + *err = HSM_SECRET_ERR_WORDLIST_FAILED; + return NULL; + } + + if (bip39_mnemonic_validate(words, line) != WALLY_OK) { + free(line); + *err = HSM_SECRET_ERR_INVALID_MNEMONIC; + return NULL; + } + + /* Convert to tal string */ + char *mnemonic = tal_strdup(ctx, line); + free(line); + + return mnemonic; +} + + + + + + diff --git a/common/hsm_secret.h b/common/hsm_secret.h new file mode 100644 index 000000000000..12fc66a91eeb --- /dev/null +++ b/common/hsm_secret.h @@ -0,0 +1,163 @@ +#ifndef LIGHTNING_COMMON_HSM_SECRET_H +#define LIGHTNING_COMMON_HSM_SECRET_H +#include "config.h" +#include +#include +#include +#include +#include + +/* Length constants for encrypted HSM secret files */ +#define HS_HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES +#define HS_CIPHERTEXT_LEN \ + (sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES) +#define ENCRYPTED_HSM_SECRET_LEN (HS_HEADER_LEN + HS_CIPHERTEXT_LEN) +#define PASSPHRASE_HASH_LEN 32 +#define HSM_SECRET_PLAIN_SIZE 32 + +enum hsm_secret_type { + HSM_SECRET_PLAIN = 0, /* Legacy 32-byte format */ + HSM_SECRET_ENCRYPTED = 1, /* Legacy 73-byte encrypted format */ + HSM_SECRET_MNEMONIC_NO_PASS = 2, /* Mnemonic without passphrase */ + HSM_SECRET_MNEMONIC_WITH_PASS = 3, /* Mnemonic with passphrase */ + HSM_SECRET_INVALID = 4, +}; + +enum hsm_secret_error { + HSM_SECRET_OK = 0, + HSM_SECRET_ERR_PASSPHRASE_REQUIRED, + HSM_SECRET_ERR_PASSPHRASE_NOT_NEEDED, + HSM_SECRET_ERR_WRONG_PASSPHRASE, + HSM_SECRET_ERR_INVALID_MNEMONIC, + HSM_SECRET_ERR_ENCRYPTION_FAILED, + HSM_SECRET_ERR_WORDLIST_FAILED, + HSM_SECRET_ERR_SEED_DERIVATION_FAILED, + HSM_SECRET_ERR_INVALID_FORMAT, + HSM_SECRET_ERR_TERMINAL, + HSM_SECRET_ERR_MEMORY +}; + +/** + * Represents the content of the hsm_secret file, either a raw seed or a mnemonic. + */ +struct hsm_secret { + struct secret secret; + char *mnemonic; /* NULL if not derived from mnemonic */ + enum hsm_secret_type type; +}; + +/** + * Checks whether the hsm_secret data requires a passphrase to decrypt. + * Handles legacy, encrypted, and mnemonic-based formats. + */ +bool hsm_secret_needs_passphrase(const u8 *hsm_secret, size_t len); + +/** + * Parse and decrypt an hsm_secret file. + * + * @ctx - a tal context + * @hsm_secret - raw file contents + * @len - length of file + * @passphrase - passphrase, or NULL if not needed + * @err - optional pointer to set error code on failure + * + * Returns parsed `struct hsm_secret` or NULL on error. + */ +struct hsm_secret *extract_hsm_secret(const tal_t *ctx, + const u8 *hsm_secret, size_t len, + const char *passphrase, + enum hsm_secret_error *err); + +/** + * Encrypt a given hsm_secret using a provided encryption key. + * @encryption_key - derived from passphrase (via Argon2) + * @hsm_secret - plaintext secret to encrypt + * @output - output buffer for encrypted data (must be ENCRYPTED_HSM_SECRET_LEN bytes) + * + * Returns true on success. + */ +bool encrypt_legacy_hsm_secret(const struct secret *encryption_key, + const struct secret *hsm_secret, + u8 *output); + +/** + * Securely discard an encryption key from memory. + * Frees memory if TAKEN. + */ +void discard_key(struct secret *key TAKES); + +/** + * Returns: + * -1: file error (sets errno) + * 0: file is not encrypted + * 1: file is encrypted + */ +int is_legacy_hsm_secret_encrypted(const char *path); + +/** + * Reads a passphrase from stdin, disabling terminal echo. + * Returns a newly allocated string on success, NULL on error. + * @ctx - tal context for allocation + * @err - on failure, this will be set to the error code + * + * Returns allocated passphrase or NULL on error. + */ +const char *read_stdin_pass(const tal_t *ctx, enum hsm_secret_error *err); + +/** + * Derive encryption key from passphrase using Argon2id. + * @ctx - tal context for allocation + * @passphrase - the passphrase to derive from + * + * Returns allocated secret or NULL on failure. + */ +struct secret *get_encryption_key(const tal_t *ctx, const char *passphrase); + +/** + * Convert error code to human-readable string. + * @err - the error code to convert + * + * Returns a string describing the error. + */ +const char *hsm_secret_error_str(enum hsm_secret_error err); + +/** + * Detect the type of hsm_secret based on its content and length. + * @hsm_secret - raw file contents + * @len - length of file + * + * Returns the detected type. + */ +enum hsm_secret_type detect_hsm_secret_type(const u8 *hsm_secret, size_t len); + +/** + * Validate passphrase for mnemonic-based secrets. + * @hsm_secret - raw file contents + * @len - length of file + * @passphrase - passphrase to validate + * + * Returns true if passphrase is valid or not needed. + */ +bool validate_mnemonic_passphrase(const u8 *hsm_secret, size_t len, const char *passphrase); + +/** + * Reads a BIP39 mnemonic from stdin with validation. + * Returns a newly allocated string on success, NULL on error. + * @ctx - tal context for allocation + * @err - optional pointer to set error code on failure + * + * Returns tal-allocated mnemonic string or NULL on error. + */ +const char *read_stdin_mnemonic(const tal_t *ctx, enum hsm_secret_error *err); + +/** + * Derive seed hash from mnemonic + passphrase. + * @mnemonic - the BIP39 mnemonic + * @passphrase - the passphrase (can be NULL) + * @seed_hash - output parameter for the derived seed hash + * + * Returns true on success, false on failure. + */ +bool derive_seed_hash(const char *mnemonic, const char *passphrase, struct sha256 *seed_hash); + +#endif /* LIGHTNING_COMMON_HSM_SECRET_H */ diff --git a/hsmd/Makefile b/hsmd/Makefile index b4f51bef2ffd..5855ceb63ed1 100644 --- a/hsmd/Makefile +++ b/hsmd/Makefile @@ -35,7 +35,7 @@ HSMD_COMMON_OBJS := \ common/daemon_conn.o \ common/derive_basepoints.o \ common/hash_u5.o \ - common/hsm_encryption.o \ + common/hsm_secret.o \ common/htlc_wire.o \ common/key_derive.o \ common/lease_rates.o \ diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index ff6d1e7b2c91..e33e245c7056 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -12,9 +12,10 @@ #include #include #include +#include #include #include -#include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include /*~ Each subdaemon is started with stdin connected to lightningd (for status @@ -35,7 +37,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,66 +272,115 @@ 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) +static void create_hsm(int fd, const char *passphrase) { - struct encrypted_hsm_secret cipher; - - if (!encrypt_hsm_secret(encryption_key, &hsm_secret, - &cipher)) + u8 *hsm_secret_data; + size_t hsm_secret_len; + + /* Always create a mnemonic-based hsm_secret */ + u8 entropy[BIP39_ENTROPY_LEN_128]; + char *mnemonic = NULL; + struct sha256 seed_hash; + + status_debug("HSM: Starting create_hsm with passphrase=%s", passphrase ? "provided" : "none"); + + /* Initialize wally tal context for libwally operations */ + tal_wally_start(); + status_debug("HSM: Initialized wally tal context"); + + /* Generate random entropy for new mnemonic */ + randombytes_buf(entropy, sizeof(entropy)); + status_debug("HSM: Generated random entropy"); + + /* Generate mnemonic from entropy */ + if (bip39_mnemonic_from_bytes(NULL, entropy, sizeof(entropy), &mnemonic) != WALLY_OK) { + tal_wally_end(tmpctx); + unlink_noerr("hsm_secret"); status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Encrypting hsm_secret"); - if (!write_all(fd, cipher.data, ENCRYPTED_HSM_SECRET_LEN)) { + "Failed to generate mnemonic from entropy"); + } + status_debug("HSM: Generated mnemonic from entropy"); + + if (!mnemonic) { + tal_wally_end(tmpctx); unlink_noerr("hsm_secret"); status_failed(STATUS_FAIL_INTERNAL_ERROR, - "Writing encrypted hsm_secret: %s", strerror(errno)); + "Failed to get generated mnemonic"); } -} - -static void create_hsm(int fd) -{ - /*~ 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. */ + + /* Derive seed hash from mnemonic + passphrase (or zero if no passphrase) */ + if (!derive_seed_hash(mnemonic, passphrase, &seed_hash)) { + tal_wally_end(tmpctx); + unlink_noerr("hsm_secret"); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Failed to derive seed hash from mnemonic"); + } + status_debug("HSM: Derived seed hash from mnemonic"); + + /* Create hsm_secret format: seed_hash (32 bytes) + mnemonic */ + hsm_secret_len = PASSPHRASE_HASH_LEN + strlen(mnemonic); + hsm_secret_data = tal_arr(tmpctx, u8, hsm_secret_len); + + /* Copy seed hash first */ + memcpy(hsm_secret_data, &seed_hash, PASSPHRASE_HASH_LEN); + /* Copy mnemonic after seed hash */ + memcpy(hsm_secret_data + PASSPHRASE_HASH_LEN, mnemonic, strlen(mnemonic)); + status_debug("HSM: Created hsm_secret data structure"); + + /* Derive the actual secret from mnemonic + passphrase for our global hsm_secret */ + u8 bip32_seed[BIP39_SEED_LEN_512]; + size_t bip32_seed_len; + + if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) { + tal_wally_end(tmpctx); + unlink_noerr("hsm_secret"); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Failed to derive seed from mnemonic"); + } + status_debug("HSM: Derived BIP32 seed from mnemonic"); + + /* Use first 32 bytes for hsm_secret */ + memcpy(&hsm_secret.secret, bip32_seed, sizeof(hsm_secret.secret)); + + /* Clean up wally allocations */ + tal_wally_end(tmpctx); + status_debug("HSM: Cleaned up wally allocations"); + + /* 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)); } + status_debug("HSM: Successfully wrote hsm_secret to file"); } /*~ 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! */ int fd = open("hsm_secret", O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) { /* If this is not the first time we've run, it will exist. */ - if (errno == EEXIST) + if (errno == EEXIST) { + status_debug("HSM: hsm_secret file already exists, skipping creation"); return; + } status_failed(STATUS_FAIL_INTERNAL_ERROR, "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); + status_debug("HSM: Creating new hsm_secret file"); + + /*~ 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 +418,50 @@ 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)); - } - } - /* 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; + status_debug("HSM: Starting load_hsm with passphrase=%s", passphrase ? "provided" : "none"); - /* hsm_control must have checked it! */ - assert(encryption_key); + /* Initialize wally tal context for libwally operations */ + tal_wally_start(); + status_debug("HSM: Initialized wally tal context for load_hsm"); - 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); - } + /* Read the hsm_secret file */ + hsm_secret_contents = grab_file(tmpctx, "hsm_secret"); + if (!hsm_secret_contents) { + tal_wally_end(tmpctx); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Could not read hsm_secret: %s", strerror(errno)); } - else - status_failed(STATUS_FAIL_INTERNAL_ERROR, "Invalid hsm_secret, " - "no plaintext nor encrypted" - " seed."); - close(fd); + status_debug("HSM: Successfully read hsm_secret file, size=%zu", tal_bytelen(hsm_secret_contents)); + + /* Remove the NUL terminator that grab_file adds */ + tal_resize(&hsm_secret_contents, tal_bytelen(hsm_secret_contents) - 1); + status_debug("HSM: Removed NUL terminator, new size=%zu", tal_bytelen(hsm_secret_contents)); + + /* Extract the secret using the new hsm_secret module */ + status_debug("HSM: Calling extract_hsm_secret"); + hsms = extract_hsm_secret(tmpctx, hsm_secret_contents, + tal_bytelen(hsm_secret_contents), + passphrase, &err); + if (!hsms) { + tal_wally_end(tmpctx); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Failed to load hsm_secret: %s", hsm_secret_error_str(err)); + } + status_debug("HSM: Successfully extracted hsm_secret"); + + /* Copy the extracted secret to our global hsm_secret */ + memcpy(&hsm_secret, &hsms->secret, sizeof(hsm_secret)); + status_debug("HSM: Copied secret to global hsm_secret"); + + /* Clean up wally allocations */ + tal_wally_end(tmpctx); + status_debug("HSM: Cleaned up wally allocations in load_hsm"); } /*~ We have a pre-init call in developer mode, to set dev flags */ @@ -457,11 +496,13 @@ static struct io_plan *init_hsm(struct io_conn *conn, struct client *c, const u8 *msg_in) { - struct secret *hsm_encryption_key; struct bip32_key_version bip32_key_version; + struct secret *hsm_encryption_key; u32 minversion, maxversion; const u32 our_minversion = 4, our_maxversion = 6; + status_debug("HSM: Starting init_hsm"); + /* This must be lightningd. */ assert(is_lightningd(c)); @@ -469,15 +510,18 @@ static struct io_plan *init_hsm(struct io_conn *conn, * definitions in hsm_client_wire.csv. The format of those files is * an extension of the simple comma-separated format output by the * BOLT tools/extract-formats.py tool. */ + struct tlv_hsmd_init_tlvs *tlvs; if (!fromwire_hsmd_init(NULL, msg_in, &bip32_key_version, &chainparams, &hsm_encryption_key, &dev_force_privkey, &dev_force_bip32_seed, &dev_force_channel_secrets, &dev_force_channel_secrets_shaseed, - &minversion, &maxversion)) + &minversion, &maxversion, &tlvs)) return bad_req(conn, c, msg_in); + status_debug("HSM: Successfully parsed init message"); + /*~ Usually we don't worry about API breakage between internal daemons, * but there are other implementations of the HSM daemon now, so we * do at least the simplest, clearest thing. */ @@ -487,14 +531,10 @@ static struct io_plan *init_hsm(struct io_conn *conn, minversion, maxversion, our_minversion, our_maxversion); - /*~ 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."); + status_debug("HSM: Version check passed"); + /*~ 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); @@ -503,19 +543,28 @@ static struct io_plan *init_hsm(struct io_conn *conn, assert(!dev_force_channel_secrets_shaseed); } + /* Extract passphrase from TLV if present */ + const char *hsm_passphrase = NULL; + if (tlvs && tlvs->hsm_passphrase) { + hsm_passphrase = (const char *)tlvs->hsm_passphrase; + status_debug("HSM: Passphrase provided in TLV"); + } else { + status_debug("HSM: No passphrase provided in TLV"); + } + /* 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)); + status_debug("HSM: About to call maybe_create_new_hsm"); + maybe_create_new_hsm(hsm_passphrase); + status_debug("HSM: About to call load_hsm"); + load_hsm(hsm_passphrase); + status_debug("HSM: Successfully loaded hsm_secret"); /* 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, + status_debug("HSM: Sending init reply"); + return req_reply(conn, c, hsmd_init(hsm_secret.secret, hsmd_mutual_version, bip32_key_version)); } diff --git a/hsmd/hsmd_wire.csv b/hsmd/hsmd_wire.csv index 4a8c7c7d9ec1..27956308380e 100644 --- a/hsmd/hsmd_wire.csv +++ b/hsmd/hsmd_wire.csv @@ -43,6 +43,9 @@ msgdata,hsmd_init_reply_v4,hsm_capabilities,u32,num_hsm_capabilities msgdata,hsmd_init_reply_v4,node_id,node_id, msgdata,hsmd_init_reply_v4,bip32,ext_key, msgdata,hsmd_init_reply_v4,bolt12,pubkey, +msgdata,hsmd_init,tlvs,hsmd_init_tlvs, +tlvtype,hsmd_init_tlvs,hsm_passphrase,1 +tlvdata,hsmd_init_tlvs,hsm_passphrase,passphrase,wirestring, # Declare a new channel. msgtype,hsmd_new_channel,30 diff --git a/lightningd/Makefile b/lightningd/Makefile index b58ceaaaf018..bc1644a829c5 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -111,7 +111,7 @@ LIGHTNINGD_COMMON_OBJS := \ common/hash_u5.o \ common/hmac.o \ common/hsm_capable.o \ - common/hsm_encryption.o \ + common/hsm_secret.o \ common/htlc_state.o \ common/htlc_trim.o \ common/htlc_tx.o \ diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c index 7c0c6587fdc0..062817365701 100644 --- a/lightningd/hsm_control.c +++ b/lightningd/hsm_control.c @@ -1,11 +1,12 @@ #include "config.h" #include #include +#include #include #include #include #include -#include +#include #include #include #include @@ -81,6 +82,25 @@ bool hsm_capable(struct lightningd *ld, u32 msgtype) return hsm_is_capable(ld->hsm_capabilities, msgtype); } +/* Read hsm passphrase if needed for mnemonic-based hsm_secret */ +static const char *read_hsm_passphrase_if_needed(struct lightningd *ld) +{ + if (!ld->hsm_passphrase_required) + return NULL; + + log_info(ld->log, "The hsm_secret uses a mnemonic with a passphrase. In order to " + "derive the seed and start the node you must provide the passphrase."); + log_info(ld->log, "Enter hsm_secret passphrase: "); + + enum hsm_secret_error err; + const char *passphrase = read_stdin_pass(tmpctx, &err); + if (err != HSM_SECRET_OK) { + fatal("Failed to read passphrase: %s", hsm_secret_error_str(err)); + } + + return passphrase; +} + struct ext_key *hsm_init(struct lightningd *ld) { u8 *msg; @@ -88,6 +108,12 @@ struct ext_key *hsm_init(struct lightningd *ld) struct ext_key *bip32_base; u32 hsm_version; struct pubkey unused; + const char *hsm_passphrase = NULL; + + /* Read passphrase if needed for mnemonic-based hsm_secret */ + if (ld->hsm_passphrase_required) { + hsm_passphrase = read_hsm_passphrase_if_needed(ld); + } /* We actually send requests synchronously: only status is async. */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0) @@ -104,7 +130,7 @@ struct ext_key *hsm_init(struct lightningd *ld) * not passed, don't let hsmd use the first 32 bytes of the cypher as the * actual secret. */ if (!ld->config.keypass) { - if (is_hsm_secret_encrypted("hsm_secret") == 1) + if (is_legacy_hsm_secret_encrypted("hsm_secret") == 1) errx(EXITCODE_HSM_ERROR_IS_ENCRYPT, "hsm_secret is encrypted, you need to pass the " "--encrypted-hsm startup option."); } @@ -127,6 +153,13 @@ struct ext_key *hsm_init(struct lightningd *ld) err(EXITCODE_HSM_GENERIC_ERROR, "Writing preinit msg to hsm"); } + /* Create TLV for passphrase if needed */ + struct tlv_hsmd_init_tlvs *tlv = NULL; + if (hsm_passphrase) { + tlv = tlv_hsmd_init_tlvs_new(tmpctx); + tlv->hsm_passphrase = tal_strdup(tlv, hsm_passphrase); + } + if (!wire_sync_write(ld->hsm_fd, towire_hsmd_init(tmpctx, &chainparams->bip32_key_version, chainparams, @@ -136,7 +169,8 @@ struct ext_key *hsm_init(struct lightningd *ld) ld->dev_force_channel_secrets, ld->dev_force_channel_secrets_shaseed, HSM_MIN_VERSION, - HSM_MAX_VERSION))) + HSM_MAX_VERSION, + tlv))) err(EXITCODE_HSM_GENERIC_ERROR, "Writing init msg to hsm"); bip32_base = tal(ld, struct ext_key); diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 68b2e2462ec5..72fdd5c948bb 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -235,6 +235,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) ld->alias = NULL; ld->rgb = NULL; ld->recover = NULL; + ld->hsm_passphrase_required = false; list_head_init(&ld->connects); list_head_init(&ld->waitsendpay_commands); list_head_init(&ld->close_commands); diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index 65383c46bdad..6e9852cb9170 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -378,7 +378,13 @@ struct lightningd { char *wallet_dsn; + + /* Whether hsm_secret requires a passphrase */ + bool hsm_passphrase_required; + + /* Legacy encrypted hsm_secret support */ bool encrypted_hsm; + struct secret *keypass; /* What (additional) messages the HSM accepts */ u32 *hsm_capabilities; diff --git a/lightningd/options.c b/lightningd/options.c index 115c7ded7eab..d9c492762c91 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include #include @@ -564,14 +564,22 @@ static void prompt(struct lightningd *ld, const char *str) * The algorithm used to derive the key is Argon2(id), to which libsodium * defaults. However argon2id-specific constants are used in case someone runs it * with a libsodium version which default constants differs (typically <1.0.9). + * + * DEPRECATED: Use --hsm-passphrase instead. */ static char *opt_set_hsm_password(struct lightningd *ld) { - char *passwd, *passwd_confirmation; - const char *err_msg; + const char *passwd, *passwd_confirmation; + enum hsm_secret_error err; int is_encrypted; - is_encrypted = is_hsm_secret_encrypted("hsm_secret"); + /* Show deprecation warning */ + if (!opt_deprecated_ok(ld, "--encrypted-hsm", + "Use --hsm-passphrase= instead", + "v25.05", "v26.05")) + return "--encrypted-hsm is deprecated, use --hsm-passphrase= instead"; + + is_encrypted = is_legacy_hsm_secret_encrypted("hsm_secret"); /* While lightningd is performing the first initialization * this check is always true because the file does not exist. * @@ -586,36 +594,51 @@ static char *opt_set_hsm_password(struct lightningd *ld) "decrypt it and start the node you must provide the password."); prompt(ld, "Enter hsm_secret password:"); - passwd = read_stdin_pass_with_exit_code(&err_msg, &opt_exitcode); - if (!passwd) - return cast_const(char *, err_msg); + passwd = read_stdin_pass(tmpctx, &err); + if (err != HSM_SECRET_OK) { + opt_exitcode = EXITCODE_HSM_PASSWORD_INPUT_ERR; + return tal_strdup(tmpctx, hsm_secret_error_str(err)); + } + if (!is_encrypted) { prompt(ld, "Confirm hsm_secret password:"); fflush(stdout); - passwd_confirmation = read_stdin_pass_with_exit_code(&err_msg, &opt_exitcode); - if (!passwd_confirmation) - return cast_const(char *, err_msg); + passwd_confirmation = read_stdin_pass(tmpctx, &err); + if (err != HSM_SECRET_OK) { + opt_exitcode = EXITCODE_HSM_PASSWORD_INPUT_ERR; + return tal_strdup(tmpctx, hsm_secret_error_str(err)); + } if (!streq(passwd, passwd_confirmation)) { opt_exitcode = EXITCODE_HSM_BAD_PASSWORD; return "Passwords confirmation mismatch."; } - free(passwd_confirmation); } prompt(ld, ""); ld->config.keypass = tal(NULL, struct secret); - opt_exitcode = hsm_secret_encryption_key_with_exitcode(passwd, ld->config.keypass, &err_msg); - if (opt_exitcode > 0) - return cast_const(char *, err_msg); + /* Derive encryption key from passphrase using the same function as hsm_secret.c */ + ld->config.keypass = get_encryption_key(tmpctx, passwd); + if (!ld->config.keypass) { + opt_exitcode = EXITCODE_HSM_BAD_PASSWORD; + return "Could not derive encryption key from password."; + } ld->encrypted_hsm = true; - free(passwd); return NULL; } +/* Set flag to indicate hsm_secret needs a passphrase. + * This replaces the old --encrypted-hsm option which was for legacy encrypted secrets. + */ +static char *opt_set_hsm_passphrase(struct lightningd *ld) +{ + ld->hsm_passphrase_required = true; + return NULL; +} + static char *opt_force_privkey(const char *optarg, struct lightningd *ld) { tal_free(ld->dev_force_privkey); @@ -1550,9 +1573,12 @@ static void register_opts(struct lightningd *ld) opt_register_early_noarg("--disable-dns", opt_set_invbool, &ld->config.use_dns, "Disable DNS lookups of peers"); + /* Deprecated: use --hsm-passphrase instead */ opt_register_noarg("--encrypted-hsm", opt_set_hsm_password, ld, - "Set the password to encrypt hsm_secret with. If no password is passed through command line, " - "you will be prompted to enter it."); + opt_hidden); + + opt_register_noarg("--hsm-passphrase", opt_set_hsm_passphrase, ld, + "Prompt for passphrase for encrypted hsm_secret (replaces --encrypted-hsm)"); opt_register_arg("--rpc-file-mode", &opt_set_mode, &opt_show_mode, &ld->rpc_filemode, @@ -1861,5 +1887,6 @@ bool is_known_opt_cb_arg(char *(*cb_arg)(const char *, void *)) || cb_arg == (void *)opt_force_privkey || cb_arg == (void *)opt_force_bip32_seed || cb_arg == (void *)opt_force_channel_secrets - || cb_arg == (void *)opt_force_tmp_channel_id; + || cb_arg == (void *)opt_force_tmp_channel_id + || cb_arg == (void *)opt_set_hsm_passphrase; } diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 63ddba2f1abe..52bb9659df6a 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1311,47 +1311,23 @@ def __init__(self, directory, *args): @unittest.skipIf(VALGRIND, "It does not play well with prompt and key derivation.") def test_hsmtool_secret_decryption(node_factory): - l1 = node_factory.get_node() - password = "reckless123#{รน}\n" + """Test that we can encrypt and decrypt hsm_secret using hsmtool""" + l1 = node_factory.get_node(start=False) # Don't start the node + password = "test_password\n" hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") - # We need to simulate a terminal to use termios in `lightningd`. - master_fd, slave_fd = os.openpty() - - # Encrypt the master seed - l1.stop() - l1.daemon.opts.update({"encrypted-hsm": None}) - l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) - l1.daemon.wait_for_log(r'Enter hsm_secret password') - write_all(master_fd, password.encode("utf-8")) - l1.daemon.wait_for_log(r'Confirm hsm_secret password') - write_all(master_fd, password.encode("utf-8")) - l1.daemon.wait_for_log("Server started with public key") - node_id = l1.rpc.getinfo()["id"] - l1.stop() - - # We can't use a wrong password ! - master_fd, slave_fd = os.openpty() - hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path) - hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log(r"Enter hsm_secret password:") - write_all(master_fd, "A wrong pass\n\n".encode("utf-8")) - hsmtool.proc.wait(WAIT_TIMEOUT) - hsmtool.is_in_log(r"Wrong password") - - # Decrypt it with hsmtool - master_fd, slave_fd = os.openpty() - hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log(r"Enter hsm_secret password:") - write_all(master_fd, password.encode("utf-8")) - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 - - # Then test we can now start it without password - l1.daemon.opts.pop("encrypted-hsm") - l1.daemon.start(stdin=slave_fd, wait_for_initialized=True) - assert node_id == l1.rpc.getinfo()["id"] - l1.stop() - - # Test we can encrypt it offline + + # Write a known 32-byte key to hsm_secret + known_secret = b'\x01' * 32 # 32 bytes of 0x01 + with open(hsm_path, 'wb') as f: + f.write(known_secret) + + # Read the hsm_secret to verify it's what we expect + with open(hsm_path, 'rb') as f: + content = f.read() + assert content == known_secret, f"Expected {known_secret}, got {content}" + assert len(content) == 32, f"Expected 32 bytes, got {len(content)}" + + # Encrypt it using hsmtool master_fd, slave_fd = os.openpty() hsmtool = HsmTool(node_factory.directory, "encrypt", hsm_path) hsmtool.start(stdin=slave_fd) @@ -1360,51 +1336,28 @@ def test_hsmtool_secret_decryption(node_factory): hsmtool.wait_for_log(r"Confirm hsm_secret password:") write_all(master_fd, password.encode("utf-8")) assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 - # Now we need to pass the encrypted-hsm startup option - l1.stop() - with pytest.raises(subprocess.CalledProcessError, match=r'returned non-zero exit status {}'.format(HSM_ERROR_IS_ENCRYPT)): - subprocess.check_call(l1.daemon.cmd_line) - - l1.daemon.opts.update({"encrypted-hsm": None}) - master_fd, slave_fd = os.openpty() - l1.daemon.start(stdin=slave_fd, - wait_for_initialized=False) - - l1.daemon.wait_for_log(r'The hsm_secret is encrypted') - write_all(master_fd, password.encode("utf-8")) - l1.daemon.wait_for_log("Server started with public key") - print(node_id, l1.rpc.getinfo()["id"]) - assert node_id == l1.rpc.getinfo()["id"] - l1.stop() - - # And finally test that we can also decrypt if encrypted with hsmtool + hsmtool.is_in_log(r"Successfully encrypted") + + # Read the hsm_secret again - it should now be encrypted (73 bytes) + with open(hsm_path, 'rb') as f: + encrypted_content = f.read() + assert len(encrypted_content) == 73, f"Expected 73 bytes after encryption, got {len(encrypted_content)}" + assert encrypted_content != known_secret, "File should be encrypted and different from original" + + # Decrypt it using hsmtool master_fd, slave_fd = os.openpty() hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path) hsmtool.start(stdin=slave_fd) hsmtool.wait_for_log(r"Enter hsm_secret password:") write_all(master_fd, password.encode("utf-8")) assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 - l1.daemon.opts.pop("encrypted-hsm") - l1.daemon.start(stdin=slave_fd, wait_for_initialized=True) - assert node_id == l1.rpc.getinfo()["id"] - - # We can roundtrip encryption and decryption using a password provided - # through stdin. - hsmtool = HsmTool(node_factory.directory, "encrypt", hsm_path) - hsmtool.start(stdin=subprocess.PIPE) - hsmtool.proc.stdin.write(password.encode("utf-8")) - hsmtool.proc.stdin.write(password.encode("utf-8")) - hsmtool.proc.stdin.flush() - hsmtool.wait_for_log("Successfully encrypted") - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 - - master_fd, slave_fd = os.openpty() - hsmtool = HsmTool(node_factory.directory, "decrypt", hsm_path) - hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log("Enter hsm_secret password:") - write_all(master_fd, password.encode("utf-8")) - hsmtool.wait_for_log("Successfully decrypted") - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + hsmtool.is_in_log(r"Successfully decrypted") + + # Read the hsm_secret again - it should now be back to the original 32 bytes + with open(hsm_path, 'rb') as f: + decrypted_content = f.read() + assert decrypted_content == known_secret, f"Expected {known_secret}, got {decrypted_content}" + assert len(decrypted_content) == 32, f"Expected 32 bytes after decryption, got {len(decrypted_content)}" @unittest.skipIf(TEST_NETWORK == 'liquid-regtest', '') @@ -1445,101 +1398,287 @@ def test_hsmtool_dump_descriptors(node_factory, bitcoind): res = bitcoind.rpc.scantxoutset("start", [{"desc": descriptor, "range": [actual_index, actual_index]}]) assert res["total_amount"] == Decimal('0.00001000') +def test_hsmtool_generatehsm_with_passphrase(node_factory): + """Test generating mnemonic-based hsm_secret with passphrase""" + l1 = node_factory.get_node(start=False, options={'hsm-passphrase': None}) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) # Remove the auto-generated one -def test_hsmtool_generatehsm(node_factory): + # Generate hsm_secret with mnemonic and passphrase + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, "test_passphrase\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + hsmtool.is_in_log(r"New hsm_secret file created") + hsmtool.is_in_log(r"Format: mnemonic with passphrase") + + # Verify file format + with open(hsm_path, 'rb') as f: + content = f.read() + # First 32 bytes should NOT be zeros (has passphrase hash) + assert content[:32] != b'\x00' * 32 + # Rest should be the mnemonic + mnemonic_part = content[32:].decode('utf-8') + assert "ritual idle hat sunny universe pluck key alpha wing cake have wedding" in mnemonic_part + + # Verify Lightning node can use it + master_fd, slave_fd = os.openpty() + l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) + write_all(master_fd, "test_passphrase\n".encode("utf-8")) + l1.daemon.wait_for_log("Server started with public key") + node_id = l1.rpc.getinfo()['id'] + assert len(node_id) == 66 # Valid node ID + l1.stop() + +def test_hsmtool_generatehsm_no_passphrase(node_factory): + """Test generating mnemonic-based hsm_secret without passphrase""" l1 = node_factory.get_node(start=False) - hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, - "hsm_secret") + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) + # Generate hsm_secret with mnemonic but no passphrase hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) - - # You cannot re-generate an already existing hsm_secret master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 2 + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, "\n".encode("utf-8")) # Empty passphrase + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + hsmtool.is_in_log(r"New hsm_secret file created") + hsmtool.is_in_log(r"Format: mnemonic without passphrase") + + # Verify file format + with open(hsm_path, 'rb') as f: + content = f.read() + # First 32 bytes should be zeros (no passphrase) + assert content[:32] == b'\x00' * 32 + # Rest should be the mnemonic + mnemonic_part = content[32:].decode('utf-8') + assert "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" in mnemonic_part + + # Verify Lightning node can use it + master_fd, slave_fd = os.openpty() + l1.daemon.start(stdin=slave_fd, wait_for_initialized=False) + l1.daemon.wait_for_log("Server started with public key") + node_id = l1.rpc.getinfo()['id'] + assert len(node_id) == 66 # Valid node ID + l1.stop() + + +def test_hsmtool_checkhsm_with_passphrase(node_factory): + """Test checkhsm with mnemonic that has a passphrase""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") os.remove(hsm_path) - # We can generate a valid hsm_secret from a wordlist and a "passphrase" + # Create hsm_secret with known mnemonic and passphrase + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log(r"Select your language:") - write_all(master_fd, "0\n".encode("utf-8")) - hsmtool.wait_for_log(r"Introduce your BIP39 word list") - write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing " - "cake have wedding\n".encode("utf-8")) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) hsmtool.wait_for_log(r"Enter your passphrase:") - write_all(master_fd, "This is actually not a passphrase\n".encode("utf-8")) - if hsmtool.proc.wait(WAIT_TIMEOUT) != 0: - hsmtool.logs_catchup() - print("hsmtool failure! Logs:") - for l in hsmtool.logs: - print(' ' + l) - assert False - hsmtool.is_in_log(r"New hsm_secret file created") + write_all(master_fd, "secret_passphrase\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 - # Check should pass. + # Test checkhsm with correct credentials hsmtool = HsmTool(node_factory.directory, "checkhsm", hsm_path) master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log(r"Enter your passphrase:") - write_all(master_fd, "This is actually not a passphrase\n".encode("utf-8")) - hsmtool.wait_for_log(r"Select your language:") - write_all(master_fd, "0\n".encode("utf-8")) - hsmtool.wait_for_log(r"Introduce your BIP39 word list") - write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing " - "cake have wedding\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter hsm_secret password:") # Decrypt file + write_all(master_fd, "secret_passphrase\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") # Backup verification + write_all(master_fd, "secret_passphrase\n".encode("utf-8")) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 hsmtool.is_in_log(r"OK") - # Wrong mnemonic will fail. + + +def test_hsmtool_checkhsm_no_passphrase(node_factory): + """Test checkhsm with mnemonic that has no passphrase""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) + + # Create hsm_secret with known mnemonic and no passphrase + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) hsmtool.wait_for_log(r"Enter your passphrase:") - write_all(master_fd, "This is actually not a passphrase\n".encode("utf-8")) - hsmtool.wait_for_log(r"Select your language:") - write_all(master_fd, "0\n".encode("utf-8")) - hsmtool.wait_for_log(r"Introduce your BIP39 word list") + write_all(master_fd, "\n".encode("utf-8")) # Empty passphrase + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Test checkhsm with correct credentials (no file unlock needed) + hsmtool = HsmTool(node_factory.directory, "checkhsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Enter your passphrase:") # Verification passphrase + write_all(master_fd, "\n".encode("utf-8")) # Empty passphrase + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") write_all(master_fd, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + hsmtool.is_in_log(r"OK") + + +def test_hsmtool_checkhsm_wrong_passphrase(node_factory): + """Test that checkhsm fails with wrong passphrase""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) + + # Create hsm_secret with known passphrase + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, "correct_passphrase\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Test checkhsm with wrong passphrase + hsmtool = HsmTool(node_factory.directory, "checkhsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Enter hsm_secret password:") # Unlock file + write_all(master_fd, "correct_passphrase\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") # Wrong verification passphrase + write_all(master_fd, "wrong_passphrase\n".encode("utf-8")) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 # ERROR_KEYDERIV hsmtool.is_in_log(r"resulting hsm_secret did not match") - # Wrong passphrase will fail. + +def test_hsmtool_checkhsm_wrong_mnemonic(node_factory): + """Test that checkhsm fails with wrong mnemonic""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) + + # Create hsm_secret with known mnemonic + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing cake have wedding\n".encode("utf-8")) hsmtool.wait_for_log(r"Enter your passphrase:") - write_all(master_fd, "This is actually not a passphrase \n".encode("utf-8")) - hsmtool.wait_for_log(r"Select your language:") - write_all(master_fd, "0\n".encode("utf-8")) - hsmtool.wait_for_log(r"Introduce your BIP39 word list") - write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing " - "cake have wedding\n".encode("utf-8")) - assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 + write_all(master_fd, "\n".encode("utf-8")) # No passphrase + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Test checkhsm with wrong mnemonic + hsmtool = HsmTool(node_factory.directory, "checkhsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, "\n".encode("utf-8")) # Correct passphrase (empty) + hsmtool.wait_for_log(r"Introduce your BIP39 word list separated by space") + write_all(master_fd, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) # Wrong mnemonic + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 # ERROR_KEYDERIV hsmtool.is_in_log(r"resulting hsm_secret did not match") - # We can start the node with this hsm_secret - l1.start() - assert l1.info['id'] == '02244b73339edd004bc6dfbb953a87984c88e9e7c02ca14ef6ec593ca6be622ba7' - l1.stop() +def test_hsmtool_generatehsm_file_exists_error(node_factory): + """Test that generatehsm fails if file already exists""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + + # File already exists from node creation + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 2 # ERROR_USAGE + hsmtool.is_in_log(r"hsm_secret file.*already exists") - # We can do the entire thing non-interactive! + +def test_hsmtool_all_commands_work_with_mnemonic_formats(node_factory): + """Test that all hsmtool commands work with mnemonic formats""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + os.remove(hsm_path) + + # Create a mnemonic-based hsm_secret (no passphrase for simplicity) + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list") + write_all(master_fd, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, "\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Test various commands work with mnemonic format + test_commands = [ + (["getnodeid", hsm_path], lambda out: len(out.strip()) == 66), + (["getcodexsecret", hsm_path, "test"], lambda out: out.strip().startswith("cl")), + (["makerune", hsm_path], lambda out: len(out.strip()) > 0), + (["dumponchaindescriptors", hsm_path], lambda out: "#" in out), # Should have checksums + ] + + for cmd_args, validator in test_commands: + cmd_line = ["tools/hsmtool"] + cmd_args + out = subprocess.check_output(cmd_line).decode("utf8") + assert validator(out), f"Command {cmd_args[0]} failed validation" + + +def test_hsmtool_deterministic_node_ids(node_factory): + """Test that same mnemonic+passphrase always produces same node ID""" + l1 = node_factory.get_node(start=False) + hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + + # Test with specific mnemonic and passphrase + mnemonic = "ritual idle hat sunny universe pluck key alpha wing cake have wedding" + passphrase = "test_passphrase" + + # Create first hsm_secret os.remove(hsm_path) - subprocess.check_output(["tools/hsmtool", - "generatehsm", hsm_path, - "en", - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"]) - assert open(hsm_path, "rb").read().hex() == "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1" + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list") + write_all(master_fd, f"{mnemonic}\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, f"{passphrase}\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Get node ID + cmd_line = ["tools/hsmtool", "getnodeid", hsm_path] + proc = subprocess.Popen(cmd_line, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + stdout, stderr = proc.communicate(input=f"{passphrase}\n".encode("utf-8")) + output = stdout.decode("utf8").strip() + # Extract the last line which should be the node ID (filter out prompt text) + first_node_id = output.split('\n')[-1] - # Including passphrase + # Create second hsm_secret with same credentials os.remove(hsm_path) - subprocess.check_output(["tools/hsmtool", - "generatehsm", hsm_path, - "en", - "ritual idle hat sunny universe pluck key alpha wing cake have wedding", - "This is actually not a passphrase"]) + hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Introduce your BIP39 word list") + write_all(master_fd, f"{mnemonic}\n".encode("utf-8")) + hsmtool.wait_for_log(r"Enter your passphrase:") + write_all(master_fd, f"{passphrase}\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + + # Get node ID again + cmd_line = ["tools/hsmtool", "getnodeid", hsm_path] + proc = subprocess.Popen(cmd_line, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + stdout, stderr = proc.communicate(input=f"{passphrase}\n".encode("utf-8")) + output = stdout.decode("utf8").strip() + # Extract the last line which should be the node ID (filter out prompt text) + second_node_id = output.split('\n')[-1] + + # Should be identical + assert first_node_id == second_node_id == '02244b73339edd004bc6dfbb953a87984c88e9e7c02ca14ef6ec593ca6be622ba7' - l1.start() - assert l1.info['id'] == '02244b73339edd004bc6dfbb953a87984c88e9e7c02ca14ef6ec593ca6be622ba7' - l1.stop() # this test does a 'listtransactions' on a yet unconfirmed channel @@ -1840,8 +1979,6 @@ def test_hsmtool_makerune(node_factory): hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path) master_fd, slave_fd = os.openpty() hsmtool.start(stdin=slave_fd) - hsmtool.wait_for_log(r"Select your language:") - write_all(master_fd, "0\n".encode("utf-8")) hsmtool.wait_for_log(r"Introduce your BIP39 word list") write_all(master_fd, "ritual idle hat sunny universe pluck key alpha wing " "cake have wedding\n".encode("utf-8")) @@ -1850,8 +1987,14 @@ def test_hsmtool_makerune(node_factory): assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 hsmtool.is_in_log(r"New hsm_secret file created") - cmd_line = ["tools/hsmtool", "makerune", hsm_path] - out = subprocess.check_output(cmd_line).decode("utf8").split("\n")[0] + # Test makerune with the passphrase + hsmtool = HsmTool(node_factory.directory, "makerune", hsm_path) + master_fd, slave_fd = os.openpty() + hsmtool.start(stdin=slave_fd) + hsmtool.wait_for_log(r"Enter hsm_secret password:") + write_all(master_fd, "This is actually not a passphrase\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + out = hsmtool.logs.split("\n")[-2] # Get the rune output (last line before empty line) l1.start() diff --git a/tools/Makefile b/tools/Makefile index dc8e2a5de65d..a952aca03a17 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -18,7 +18,7 @@ tools/headerversions: $(FORCE) tools/headerversions.o libccan.a tools/headerversions.o: ccan/config.h tools/check-bolt: tools/check-bolt.o $(TOOLS_COMMON_OBJS) -tools/hsmtool: tools/hsmtool.o $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/autodata.o common/bech32.o common/bech32_util.o common/bigsize.o common/codex32.o common/configdir.o common/configvar.o common/derive_basepoints.o common/descriptor_checksum.o common/hsm_encryption.o common/node_id.o common/version.o wire/fromwire.o wire/towire.o +tools/hsmtool: tools/hsmtool.o $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/autodata.o common/bech32.o common/bech32_util.o common/bigsize.o common/codex32.o common/configdir.o common/configvar.o common/derive_basepoints.o common/descriptor_checksum.o common/hsm_secret.o common/node_id.o common/version.o wire/fromwire.o wire/towire.o tools/lightning-hsmtool: tools/hsmtool cp $< $@ diff --git a/tools/hsmtool.c b/tools/hsmtool.c index 94ee25fdbba1..9140af3b54e2 100644 --- a/tools/hsmtool.c +++ b/tools/hsmtool.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,12 +17,13 @@ #include #include #include -#include +#include #include #include #include #include #include +#include #include #include #include @@ -37,13 +39,11 @@ static void show_usage(const char *progname) { printf("%s [arguments]\n", progname); printf("methods:\n"); - printf(" - decrypt \n"); - printf(" - encrypt \n"); - printf(" - dumpcommitments " - "\n"); - printf(" - guesstoremote " - "\n"); - printf(" - generatehsm [ []]\n"); + printf(" - decrypt [LEGACY - binary format only]\n"); + printf(" - encrypt [LEGACY - binary format only]\n"); + printf(" - dumpcommitments \n"); + printf(" - guesstoremote \n"); + printf(" - generatehsm \n"); printf(" - checkhsm \n"); printf(" - dumponchaindescriptors [--show-secrets] [network]\n"); printf(" - makerune \n"); @@ -53,6 +53,23 @@ static void show_usage(const char *progname) exit(0); } +static const char *format_type_name(enum hsm_secret_type type) +{ + switch (type) { + case HSM_SECRET_PLAIN: + return "plain (32-byte binary)"; + case HSM_SECRET_ENCRYPTED: + return "encrypted (73-byte binary)"; + case HSM_SECRET_MNEMONIC_NO_PASS: + return "mnemonic (no password)"; + case HSM_SECRET_MNEMONIC_WITH_PASS: + return "mnemonic (with password)"; + case HSM_SECRET_INVALID: + return "invalid"; + } + return "unknown"; +} + static bool ensure_hsm_secret_exists(int fd, const char *path) { const char *config_dir = path_dirname(NULL, path); @@ -75,161 +92,71 @@ static bool ensure_hsm_secret_exists(int fd, const char *path) tal_free(config_dir); return true; } - -static void grab_hsm_file(const char *hsm_secret_path, - void *dst, size_t dstlen) +/* Load hsm_secret using the unified interface */ +static struct hsm_secret *load_hsm_secret(const tal_t *ctx, const char *hsm_secret_path) { u8 *contents = grab_file(tmpctx, hsm_secret_path); + const char *passphrase = NULL; + struct hsm_secret *hsms; + enum hsm_secret_error err; + if (!contents) errx(EXITCODE_ERROR_HSM_FILE, "Reading hsm_secret"); - /* grab_file always appends a NUL char for convenience */ - if (tal_bytelen(contents) != dstlen + 1) - errx(EXITCODE_ERROR_HSM_FILE, - "hsm_secret invalid length %zu (expected %zu)", - tal_bytelen(contents)-1, dstlen); - memcpy(dst, contents, dstlen); -} - -static void get_unencrypted_hsm_secret(struct secret *hsm_secret, - const char *hsm_secret_path) -{ - grab_hsm_file(hsm_secret_path, hsm_secret, sizeof(*hsm_secret)); -} - -/* Derive the encryption key from the password provided, and try to decrypt - * the cipher. */ -static void get_encrypted_hsm_secret(struct secret *hsm_secret, - const char *hsm_secret_path, - const char *passwd) -{ - struct secret key; - struct encrypted_hsm_secret encrypted_secret; - const char *err; - int exit_code; + /* Remove the NUL terminator that grab_file adds */ + tal_resize(&contents, tal_bytelen(contents) - 1); - grab_hsm_file(hsm_secret_path, - &encrypted_secret, sizeof(encrypted_secret)); + /* Get passphrase if needed */ + if (hsm_secret_needs_passphrase(contents, tal_bytelen(contents))) { + printf("Enter hsm_secret password:\n"); + fflush(stdout); + passphrase = read_stdin_pass(tmpctx, &err); + if (!passphrase) + errx(EXITCODE_ERROR_HSM_FILE, "Could not read password: %s", hsm_secret_error_str(err)); + } - exit_code = hsm_secret_encryption_key_with_exitcode(passwd, &key, &err); - if (exit_code > 0) - errx(exit_code, "%s", err); - if (!decrypt_hsm_secret(&key, &encrypted_secret, hsm_secret)) - errx(ERROR_LIBSODIUM, "Could not retrieve the seed. Wrong password ?"); + hsms = extract_hsm_secret(ctx, contents, tal_bytelen(contents), passphrase, &err); + if (!hsms) { + errx(EXITCODE_ERROR_HSM_FILE, "%s", hsm_secret_error_str(err)); + } + return hsms; } -/* Taken from hsmd. */ -static void get_channel_seed(struct secret *channel_seed, struct node_id *peer_id, - u64 dbid, struct secret *hsm_secret) +/* Legacy function - only works with binary encrypted format */ +static int decrypt_hsm(const char *hsm_secret_path) { - struct secret channel_base; - u8 input[sizeof(peer_id->k) + sizeof(dbid)]; - /*~ Again, "per-peer" should be "per-channel", but Hysterical Raisins */ - const char *info = "per-peer seed"; + int fd; + struct hsm_secret *hsms; + const char *dir, *backup; - /*~ We use the DER encoding of the pubkey, because it's platform - * independent. Since the dbid is unique, however, it's completely - * unnecessary, but again, existing users can't be broken. */ - /* FIXME: lnd has a nicer BIP32 method for deriving secrets which we - * should migrate to. */ - hkdf_sha256(&channel_base, sizeof(struct secret), NULL, 0, - hsm_secret, sizeof(*hsm_secret), - /*~ Initially, we didn't support multiple channels per - * peer at all: a channel had to be completely forgotten - * before another could exist. That was slightly relaxed, - * but the phrase "peer seed" is wired into the seed - * generation here, so we need to keep it that way for - * existing clients, rather than using "channel seed". */ - "peer seed", strlen("peer seed")); - memcpy(input, peer_id->k, sizeof(peer_id->k)); - BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); - /*~ For all that talk about platform-independence, note that this - * field is endian-dependent! But let's face it, little-endian won. - * In related news, we don't support EBCDIC or middle-endian. */ - memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); + /* Check if it's a format we can decrypt */ + u8 *contents = grab_file(tmpctx, hsm_secret_path); + if (!contents) + errx(EXITCODE_ERROR_HSM_FILE, "Reading hsm_secret"); - hkdf_sha256(channel_seed, sizeof(*channel_seed), - input, sizeof(input), - &channel_base, sizeof(channel_base), - info, strlen(info)); -} + tal_resize(&contents, tal_bytelen(contents) - 1); + enum hsm_secret_type type = detect_hsm_secret_type(contents, tal_bytelen(contents)); -/* We detect an encrypted hsm_secret as a hsm_secret which is 73-bytes long. */ -static bool hsm_secret_is_encrypted(const char *hsm_secret_path) -{ - switch (is_hsm_secret_encrypted(hsm_secret_path)) { - case -1: - err(EXITCODE_ERROR_HSM_FILE, "Cannot open '%s'", hsm_secret_path); - case 1: - return true; - case 0: { - /* Extra sanity check on HSM file! */ - struct stat st; - stat(hsm_secret_path, &st); - if (st.st_size != 32) - errx(EXITCODE_ERROR_HSM_FILE, - "Invalid hsm_secret '%s' (neither plaintext " - "nor encrypted).", hsm_secret_path); - return false; - } + if (type != HSM_SECRET_ENCRYPTED) { + errx(ERROR_USAGE, "decrypt command only works on legacy encrypted binary format (73 bytes).\n" + "Current file is: %s\n" + "For mnemonic formats, use the generatehsm command to create a new hsm_secret instead.", + format_type_name(type)); } - abort(); -} - -/* If encrypted, ask for a passphrase */ -static void get_hsm_secret(struct secret *hsm_secret, - const char *hsm_secret_path) -{ - /* This checks the file existence, too. */ - if (hsm_secret_is_encrypted(hsm_secret_path)) { - int exit_code; - char *passwd; - const char *err; - - printf("Enter hsm_secret password:\n"); - fflush(stdout); - passwd = read_stdin_pass_with_exit_code(&err, &exit_code); - if (!passwd) - errx(exit_code, "%s", err); - get_encrypted_hsm_secret(hsm_secret, hsm_secret_path, passwd); - free(passwd); - } else { - get_unencrypted_hsm_secret(hsm_secret, hsm_secret_path); - } -} - -static int decrypt_hsm(const char *hsm_secret_path) -{ - int fd; - struct secret hsm_secret; - char *passwd; - const char *dir, *backup, *err; - int exit_code = 0; - /* This checks the file existence, too. */ - if (!hsm_secret_is_encrypted(hsm_secret_path)) - errx(ERROR_USAGE, "hsm_secret is not encrypted"); - printf("Enter hsm_secret password:\n"); - fflush(stdout); - passwd = read_stdin_pass_with_exit_code(&err, &exit_code); - if (!passwd) - errx(exit_code, "%s", err); + /* Load the hsm_secret */ + hsms = load_hsm_secret(tmpctx, hsm_secret_path); dir = path_dirname(NULL, hsm_secret_path); backup = path_join(dir, dir, "hsm_secret.backup"); - get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd); - /* Once the encryption key derived, we don't need it anymore. */ - if (passwd) - free(passwd); - /* Create a backup file, "just in case". */ rename(hsm_secret_path, backup); fd = open(hsm_secret_path, O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) errx(EXITCODE_ERROR_HSM_FILE, "Could not open new hsm_secret"); - if (!write_all(fd, &hsm_secret, sizeof(hsm_secret))) { + if (!write_all(fd, &hsms->secret, sizeof(hsms->secret))) { unlink_noerr(hsm_secret_path); close(fd); rename("hsm_secret.backup", hsm_secret_path); @@ -251,85 +178,62 @@ static int decrypt_hsm(const char *hsm_secret_path) return 0; } -static int make_codexsecret(const char *hsm_secret_path, - const char *id) +/* Legacy function - only works with binary plain format */ +static int encrypt_hsm(const char *hsm_secret_path) { - struct secret hsm_secret; - char *bip93; - const char *err; - get_hsm_secret(&hsm_secret, hsm_secret_path); + int fd; + struct hsm_secret *hsms; + u8 encrypted_hsm_secret[ENCRYPTED_HSM_SECRET_LEN]; + const char *passwd, *passwd_confirmation; + const char *dir, *backup; + enum hsm_secret_error pass_err; - err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93); - if (err) - errx(ERROR_USAGE, "%s", err); + /* Check if it's a format we can encrypt */ + u8 *contents = grab_file(tmpctx, hsm_secret_path); + if (!contents) + errx(EXITCODE_ERROR_HSM_FILE, "Reading hsm_secret"); - printf("%s\n", bip93); - return 0; -} + tal_resize(&contents, tal_bytelen(contents) - 1); + enum hsm_secret_type type = detect_hsm_secret_type(contents, tal_bytelen(contents)); -static int getemergencyrecover(const char *emer_rec_path) -{ - u8 *scb = grab_file(tmpctx, emer_rec_path); - char *output, *hrp = "clnemerg"; - if (!scb) { - errx(EXITCODE_ERROR_HSM_FILE, "Reading emergency.recover"); - } else { - /* grab_file adds nul term */ - tal_resize(&scb, tal_bytelen(scb) - 1); + if (type != HSM_SECRET_PLAIN) { + errx(ERROR_USAGE, "encrypt command only works on legacy plain binary format (32 bytes).\n" + "Current file is: %s\n" + "For mnemonic formats, the passphrase is already integrated into the format.", + format_type_name(type)); } - u5 *data = tal_arr(tmpctx, u5, 0); - - bech32_push_bits(&data, scb, tal_bytelen(scb) * 8); - output = tal_arr(tmpctx, char, strlen(hrp) + tal_count(data) + 8); - - bech32_encode(output, hrp, data, tal_count(data), (size_t)-1, - BECH32_ENCODING_BECH32); - - printf("%s\n", output); - return 0; -} - -static int encrypt_hsm(const char *hsm_secret_path) -{ - int fd; - struct secret key, hsm_secret; - struct encrypted_hsm_secret encrypted_hsm_secret; - char *passwd, *passwd_confirmation; - const char *err, *dir, *backup; - int exit_code = 0; - /* This checks the file existence, too. */ - if (hsm_secret_is_encrypted(hsm_secret_path)) - errx(ERROR_USAGE, "hsm_secret is already encrypted"); + /* Load the hsm_secret */ + hsms = load_hsm_secret(tmpctx, hsm_secret_path); printf("Enter hsm_secret password:\n"); fflush(stdout); - passwd = read_stdin_pass_with_exit_code(&err, &exit_code); + passwd = read_stdin_pass(tmpctx, &pass_err); if (!passwd) - errx(exit_code, "%s", err); + errx(EXITCODE_ERROR_HSM_FILE, "Could not read password: %s", hsm_secret_error_str(pass_err)); + printf("Confirm hsm_secret password:\n"); fflush(stdout); - passwd_confirmation = read_stdin_pass_with_exit_code(&err, &exit_code); + passwd_confirmation = read_stdin_pass(tmpctx, &pass_err); if (!passwd_confirmation) - errx(exit_code, "%s", err); + errx(EXITCODE_ERROR_HSM_FILE, "Could not read password: %s", hsm_secret_error_str(pass_err)); + if (!streq(passwd, passwd_confirmation)) errx(ERROR_USAGE, "Passwords confirmation mismatch."); - get_unencrypted_hsm_secret(&hsm_secret, hsm_secret_path); dir = path_dirname(NULL, hsm_secret_path); backup = path_join(dir, dir, "hsm_secret.backup"); - /* Derive the encryption key from the password provided, and try to encrypt - * the seed. */ - exit_code = hsm_secret_encryption_key_with_exitcode(passwd, &key, &err); - if (exit_code > 0) - errx(exit_code, "%s", err); - if (!encrypt_hsm_secret(&key, &hsm_secret, &encrypted_hsm_secret)) + /* Create encryption key and encrypt */ + struct secret *encryption_key = get_encryption_key(tmpctx, passwd); + if (!encryption_key) + errx(ERROR_LIBSODIUM, "Could not derive encryption key"); + + if (!encrypt_legacy_hsm_secret(encryption_key, &hsms->secret, encrypted_hsm_secret)) errx(ERROR_LIBSODIUM, "Could not encrypt the hsm_secret seed."); - /* Once the encryption key derived, we don't need it anymore. */ - free(passwd); - free(passwd_confirmation); + /* Securely discard the encryption key */ + discard_key(encryption_key); /* Create a backup file, "just in case". */ rename(hsm_secret_path, backup); @@ -338,8 +242,8 @@ static int encrypt_hsm(const char *hsm_secret_path) errx(EXITCODE_ERROR_HSM_FILE, "Could not open new hsm_secret"); /* Write the encrypted hsm_secret. */ - if (!write_all(fd, encrypted_hsm_secret.data, - sizeof(encrypted_hsm_secret.data))) { + if (!write_all(fd, encrypted_hsm_secret, + ENCRYPTED_HSM_SECRET_LEN)) { unlink_noerr(hsm_secret_path); close(fd); rename(backup, hsm_secret_path); @@ -360,14 +264,73 @@ static int encrypt_hsm(const char *hsm_secret_path) return 0; } +/* Taken from hsmd. */ +static void get_channel_seed(struct secret *channel_seed, struct node_id *peer_id, + u64 dbid, struct secret *hsm_secret) +{ + struct secret channel_base; + u8 input[sizeof(peer_id->k) + sizeof(dbid)]; + const char *info = "per-peer seed"; + + hkdf_sha256(&channel_base, sizeof(struct secret), NULL, 0, + hsm_secret, sizeof(*hsm_secret), + "peer seed", strlen("peer seed")); + memcpy(input, peer_id->k, sizeof(peer_id->k)); + BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); + memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); + + hkdf_sha256(channel_seed, sizeof(*channel_seed), + input, sizeof(input), + &channel_base, sizeof(channel_base), + info, strlen(info)); +} + +static int make_codexsecret(const char *hsm_secret_path, const char *id) +{ + struct secret hsm_secret; + char *bip93; + const char *err; + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; + + err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93); + if (err) + errx(ERROR_USAGE, "%s", err); + + printf("%s\n", bip93); + return 0; +} + +static int getemergencyrecover(const char *emer_rec_path) +{ + u8 *scb = grab_file(tmpctx, emer_rec_path); + char *output, *hrp = "clnemerg"; + if (!scb) { + errx(EXITCODE_ERROR_HSM_FILE, "Reading emergency.recover"); + } else { + /* grab_file adds nul term */ + tal_resize(&scb, tal_bytelen(scb) - 1); + } + u5 *data = tal_arr(tmpctx, u5, 0); + + bech32_push_bits(&data, scb, tal_bytelen(scb) * 8); + output = tal_arr(tmpctx, char, strlen(hrp) + tal_count(data) + 8); + + bech32_encode(output, hrp, data, tal_count(data), (size_t)-1, + BECH32_ENCODING_BECH32); + + printf("%s\n", output); + return 0; +} + static int dump_commitments_infos(struct node_id *node_id, u64 channel_id, u64 depth, char *hsm_secret_path) { struct sha256 shaseed; struct secret hsm_secret, channel_seed, per_commitment_secret; struct pubkey per_commitment_point; - - get_hsm_secret(&hsm_secret, hsm_secret_path); + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; get_channel_seed(&channel_seed, node_id, channel_id, &hsm_secret); derive_shaseed(&channel_seed, &shaseed); @@ -387,31 +350,13 @@ static int dump_commitments_infos(struct node_id *node_id, u64 channel_id, return 0; } -/* In case of an unilateral close from the remote side while we suffered a - * loss of data, this tries to recover the private key from the `to_remote` - * output. - * This basically iterates over every `dbid` to derive the channel_seed and - * then derives the payment basepoint to compare to the pubkey hash specified - * in the witness programm. - * Note that since a node generates the key for the to_remote output from its - * *local* per_commitment_point, there is nothing we can do if - * `option_static_remotekey` was not negotiated. - * - * :param address: The bech32 address of the v0 P2WPKH witness programm - * :param node_id: The id of the node with which the channel was established - * :param tries: How many dbids to try. - * :param hsm_secret_path: The path to the hsm_secret - * :param passwd: The *optional* hsm_secret password - */ static int guess_to_remote(const char *address, struct node_id *node_id, u64 tries, char *hsm_secret_path) { struct secret hsm_secret, channel_seed, basepoint_secret; struct pubkey basepoint; struct ripemd160 pubkeyhash; - /* We only support P2WPKH, hence 20. */ u8 goal_pubkeyhash[20]; - /* See common/bech32.h for buffer size. */ char hrp[strlen(address) - 6]; int witver; size_t witlen; @@ -422,7 +367,8 @@ static int guess_to_remote(const char *address, struct node_id *node_id, if (segwit_addr_decode(&witver, goal_pubkeyhash, &witlen, hrp, address) != 1) errx(ERROR_USAGE, "Wrong bech32 address"); - get_hsm_secret(&hsm_secret, hsm_secret_path); + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; for (u64 dbid = 1; dbid < tries ; dbid++) { get_channel_seed(&channel_seed, node_id, dbid, &hsm_secret); @@ -450,147 +396,65 @@ static int guess_to_remote(const char *address, struct node_id *node_id, return 1; } -struct wordlist_lang { - char *abbr; - char *name; -}; - -struct wordlist_lang languages[] = { - {"en", "English"}, - {"es", "Spanish"}, - {"fr", "French"}, - {"it", "Italian"}, - {"jp", "Japanese"}, - {"zhs", "Chinese Simplified"}, - {"zht", "Chinese Traditional"}, -}; - -static bool check_lang(const char *abbr) +static int generate_hsm(const char *hsm_secret_path) { - for (size_t i = 0; i < ARRAY_SIZE(languages); i++) { - if (streq(abbr, languages[i].abbr)) - return true; - } - return false; -} + const char *mnemonic, *passphrase; + enum hsm_secret_error err; -static void get_words(struct words **words) { - - printf("Select your language:\n"); - for (size_t i = 0; i < ARRAY_SIZE(languages); i++) { - printf(" %zu) %s (%s)\n", i, languages[i].name, languages[i].abbr); - } - printf("Select [0-%zu]: ", ARRAY_SIZE(languages)-1); - fflush(stdout); - - char *selected = NULL; - size_t size = 0; - size_t characters = getline(&selected, &size, stdin); - if (characters < 0) - errx(ERROR_USAGE, "Could not read line from stdin."); - - /* To distinguish success/failure after call */ - errno = 0; - char *endptr; - long val = strtol(selected, &endptr, 10); - if (errno == ERANGE || (errno != 0 && val == 0) || endptr == selected || val < 0 || val >= ARRAY_SIZE(languages)) - errx(ERROR_USAGE, "Invalid language selection, select one from the list [0-6]."); - - free(selected); - bip39_get_wordlist(languages[val].abbr, words); -} + /* Get mnemonic from user using consistent interface */ + mnemonic = read_stdin_mnemonic(tmpctx, &err); + if (!mnemonic) + errx(EXITCODE_ERROR_HSM_FILE, "Could not read mnemonic: %s", hsm_secret_error_str(err)); -static char *get_mnemonic(void) { - char *line = NULL; - size_t line_size = 0; - - printf("Introduce your BIP39 word list separated by space (at least 12 words):\n"); + /* Get optional passphrase */ + printf("Warning: remember that different passphrases yield different " + "bitcoin wallets.\n"); + printf("If left empty, no password is used (echo is disabled).\n"); + printf("Enter your passphrase: \n"); fflush(stdout); - size_t characters = getline(&line, &line_size, stdin); - if (characters < 0) - errx(ERROR_USAGE, "Could not read line from stdin."); - line[characters-1] = '\0'; - return line; -} - -static char *read_mnemonic(void) { - /* Get words for the mnemonic language */ - struct words *words; - get_words(&words); - - /* Get mnemonic */ - char *mnemonic; - mnemonic = get_mnemonic(); - - if (bip39_mnemonic_validate(words, mnemonic) != 0) { - errx(ERROR_USAGE, "Invalid mnemonic: \"%s\"", mnemonic); - } - return mnemonic; -} - -static int generate_hsm(const char *hsm_secret_path, - const char *lang_id, - char *mnemonic, - char *passphrase) -{ - const char *err; - int exit_code = 0; - - if (lang_id == NULL) { - mnemonic = read_mnemonic(); - printf("Warning: remember that different passphrases yield different " - "bitcoin wallets.\n"); - printf("If left empty, no password is used (echo is disabled).\n"); - printf("Enter your passphrase: \n"); - fflush(stdout); - passphrase = read_stdin_pass_with_exit_code(&err, &exit_code); - if (!passphrase) - errx(exit_code, "%s", err); - if (strlen(passphrase) == 0) { - free(passphrase); - passphrase = NULL; - } - } else { - struct words *words; - - bip39_get_wordlist(lang_id, &words); - - if (bip39_mnemonic_validate(words, mnemonic) != 0) - errx(ERROR_USAGE, "Invalid mnemonic: \"%s\"", mnemonic); + passphrase = read_stdin_pass(tmpctx, &err); + if (!passphrase) + errx(EXITCODE_ERROR_HSM_FILE, "Could not read passphrase: %s", hsm_secret_error_str(err)); + if (strlen(passphrase) == 0) { + passphrase = NULL; } - u8 bip32_seed[BIP39_SEED_LEN_512]; - size_t bip32_seed_len; - - if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) - errx(ERROR_LIBWALLY, "Unable to derive BIP32 seed from BIP39 mnemonic"); - + /* Write to file using your new mnemonic format */ int fd = open(hsm_secret_path, O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) { errx(ERROR_USAGE, "Unable to create hsm_secret file"); } - /* Write only the first 32 bytes, length of the (plaintext) seed in the - * hsm_secret. */ - if (!write_all(fd, bip32_seed, 32)) - errx(ERROR_USAGE, "Error writing secret to hsm_secret file"); + + /* Hash the derived seed for validation */ + struct sha256 seed_hash; + if (!derive_seed_hash(mnemonic, passphrase, &seed_hash)) + errx(ERROR_USAGE, "Error deriving seed from mnemonic"); + + /* Write seed hash (32 bytes) + mnemonic */ + if (!write_all(fd, &seed_hash, sizeof(seed_hash))) + errx(ERROR_USAGE, "Error writing seed hash to hsm_secret file"); + + /* Write the mnemonic */ + if (!write_all(fd, mnemonic, strlen(mnemonic))) + errx(ERROR_USAGE, "Error writing mnemonic to hsm_secret file"); if (fsync(fd) != 0) errx(ERROR_USAGE, "Error fsyncing hsm_secret file"); - /* This should never fail if fsync succeeded. But paranoia is good, and bugs exist */ if (close(fd) != 0) errx(ERROR_USAGE, "Error closing hsm_secret file"); printf("New hsm_secret file created at %s\n", hsm_secret_path); - printf("Use the `encrypt` command to encrypt the BIP32 seed if needed\n"); + printf("Format: %s\n", passphrase ? "mnemonic with passphrase" : "mnemonic without passphrase"); + if (passphrase) { + printf("Remember your passphrase - it's required to use this hsm_secret!\n"); + } - free(mnemonic); - free(passphrase); + /* passphrase and mnemonic will be automatically cleaned up by tmpctx */ return 0; } static int dumponchaindescriptors(const char *hsm_secret_path, - const char *old_passwd UNUSED, const u32 version, bool show_secrets) { struct secret hsm_secret; @@ -599,11 +463,9 @@ static int dumponchaindescriptors(const char *hsm_secret_path, struct ext_key master_extkey; char *enc_xkey, *descriptor; struct descriptor_checksum checksum; - - get_hsm_secret(&hsm_secret, hsm_secret_path); - - /* We use m/0/0/k as the derivation tree for onchain funds. */ - + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; + /* The root seed is derived from hsm_secret using hkdf.. */ do { hkdf_sha256(bip32_seed, sizeof(bip32_seed), @@ -653,41 +515,54 @@ static int dumponchaindescriptors(const char *hsm_secret_path, static int check_hsm(const char *hsm_secret_path) { - char *mnemonic; - struct secret hsm_secret; + struct secret file_secret, derived_secret; u8 bip32_seed[BIP39_SEED_LEN_512]; size_t bip32_seed_len; - int exit_code; - char *passphrase; - const char *err; + const char *passphrase, *mnemonic; + enum hsm_secret_error err; - get_hsm_secret(&hsm_secret, hsm_secret_path); + /* Check what type of hsm_secret we're dealing with */ + u8 *contents = grab_file(tmpctx, hsm_secret_path); + if (!contents) + errx(EXITCODE_ERROR_HSM_FILE, "Reading hsm_secret"); + tal_resize(&contents, tal_bytelen(contents) - 1); + + /* Get the actual seed from the file */ + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + file_secret = hsms->secret; + /* Ask user for their BIP39 backup passphrase */ printf("Warning: remember that different passphrases yield different " "bitcoin wallets.\n"); printf("If left empty, no password is used (echo is disabled).\n"); printf("Enter your passphrase: \n"); fflush(stdout); - passphrase = read_stdin_pass_with_exit_code(&err, &exit_code); + passphrase = read_stdin_pass(tmpctx, &err); if (!passphrase) - errx(exit_code, "%s", err); + errx(EXITCODE_ERROR_HSM_FILE, "Could not read passphrase: %s", hsm_secret_error_str(err)); if (strlen(passphrase) == 0) { - free(passphrase); passphrase = NULL; } - mnemonic = read_mnemonic(); + /* Ask user for their backup mnemonic using consistent interface */ + mnemonic = read_stdin_mnemonic(tmpctx, &err); + if (!mnemonic) + errx(EXITCODE_ERROR_HSM_FILE, "Could not read mnemonic: %s", hsm_secret_error_str(err)); + + /* Derive seed from user's backup mnemonic + passphrase */ if (bip39_mnemonic_to_seed(mnemonic, passphrase, bip32_seed, sizeof(bip32_seed), &bip32_seed_len) != WALLY_OK) errx(ERROR_LIBWALLY, "Unable to derive BIP32 seed from BIP39 mnemonic"); - /* We only use first 32 bytes */ - if (memcmp(bip32_seed, hsm_secret.data, sizeof(hsm_secret.data)) != 0) + /* Copy first 32 bytes to our secret for comparison */ + memcpy(derived_secret.data, bip32_seed, sizeof(derived_secret.data)); + + /* Compare the seeds - this works for all formats */ + if (memcmp(derived_secret.data, file_secret.data, sizeof(file_secret.data)) != 0) errx(ERROR_KEYDERIV, "resulting hsm_secret did not match"); printf("OK\n"); - free(mnemonic); - free(passphrase); + /* passphrase and mnemonic will be automatically cleaned up by tmpctx */ return 0; } @@ -695,9 +570,8 @@ static int make_rune(const char *hsm_secret_path) { struct secret hsm_secret, derived_secret, rune_secret; struct rune *master_rune, *rune; - - /* Get hsm_secret */ - get_hsm_secret(&hsm_secret, hsm_secret_path); + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; /* HSM derives a root secret for `makesecret` */ hkdf_sha256(&derived_secret, sizeof(struct secret), NULL, 0, @@ -724,9 +598,8 @@ static int get_node_id(const char *hsm_secret_path) struct secret hsm_secret; struct privkey node_privkey; struct pubkey node_id; - - /* Get hsm_secret */ - get_hsm_secret(&hsm_secret, hsm_secret_path); + struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsm_secret = hsms->secret; /*~ So, there is apparently a 1 in 2^127 chance that a random value is * not a valid private key, so this never actually loops. */ @@ -795,12 +668,10 @@ int main(int argc, char *argv[]) } if (streq(method, "generatehsm")) { - // argv[2] file, argv[3] lang_id, argv[4] word list, argv[5] passphrase - if (argc < 3 || argc > 6 || argc == 4) + if (argc != 3) show_usage(argv[0]); char *hsm_secret_path = argv[2]; - char *lang_id, *word_list, *passphrase; /* if hsm_secret already exists we abort the process * we do not want to lose someone else's funds */ @@ -808,15 +679,7 @@ int main(int argc, char *argv[]) if (stat(hsm_secret_path, &st) == 0) errx(ERROR_USAGE, "hsm_secret file at %s already exists", hsm_secret_path); - lang_id = (argc > 3 ? argv[3] : NULL); - if (lang_id && !check_lang(lang_id)) - show_usage(argv[0]); - - /* generate_hsm expects to free these, so use strdup */ - word_list = (argc > 4 ? strdup(argv[4]) : NULL); - passphrase = (argc > 5 ? strdup(argv[5]) : NULL); - - return generate_hsm(hsm_secret_path, lang_id, word_list, passphrase); + return generate_hsm(hsm_secret_path); } if (streq(method, "dumponchaindescriptors")) { @@ -869,7 +732,7 @@ int main(int argc, char *argv[]) else version = BIP32_VER_MAIN_PRIVATE; - return dumponchaindescriptors(fname, NULL, version, show_secrets); + return dumponchaindescriptors(fname, version, show_secrets); } if (streq(method, "checkhsm")) { @@ -903,4 +766,4 @@ int main(int argc, char *argv[]) } show_usage(argv[0]); -} +} \ No newline at end of file diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 5787f640c25f..c7ecdf78110a 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -435,9 +435,15 @@ bool invoices_resolve(struct invoices *invoices UNNEEDED, const struct json_escape *label UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "invoices_resolve called!\n"); abort(); } -/* Generated stub for is_hsm_secret_encrypted */ -int is_hsm_secret_encrypted(const char *path UNNEEDED) -{ fprintf(stderr, "is_hsm_secret_encrypted called!\n"); abort(); } +/* Generated stub for is_legacy_hsm_secret_encrypted */ +int is_legacy_hsm_secret_encrypted(const char *path UNNEEDED) +{ fprintf(stderr, "is_legacy_hsm_secret_encrypted called!\n"); abort(); } +/* Generated stub for hsm_secret_error_str */ +const char *hsm_secret_error_str(enum hsm_secret_error err UNNEEDED) +{ fprintf(stderr, "hsm_secret_error_str called!\n"); abort(); } +/* Generated stub for read_stdin_pass */ +const char *read_stdin_pass(const tal_t *ctx UNNEEDED, enum hsm_secret_error *err UNNEEDED) +{ fprintf(stderr, "read_stdin_pass called!\n"); abort(); } /* Generated stub for json_add_address */ void json_add_address(struct json_stream *response UNNEEDED, const char *fieldname UNNEEDED, const struct wireaddr *addr UNNEEDED) @@ -1141,7 +1147,7 @@ u8 *towire_hsmd_forget_channel(const tal_t *ctx UNNEEDED, const struct node_id * u8 *towire_hsmd_get_output_scriptpubkey(const tal_t *ctx UNNEEDED, u64 channel_id UNNEEDED, const struct node_id *peer_id UNNEEDED, const struct pubkey *commitment_point UNNEEDED) { fprintf(stderr, "towire_hsmd_get_output_scriptpubkey called!\n"); abort(); } /* Generated stub for towire_hsmd_init */ -u8 *towire_hsmd_init(const tal_t *ctx UNNEEDED, const struct bip32_key_version *bip32_key_version UNNEEDED, const struct chainparams *chainparams UNNEEDED, const struct secret *hsm_encryption_key UNNEEDED, const struct privkey *dev_force_privkey UNNEEDED, const struct secret *dev_force_bip32_seed UNNEEDED, const struct secrets *dev_force_channel_secrets UNNEEDED, const struct sha256 *dev_force_channel_secrets_shaseed UNNEEDED, u32 hsm_wire_min_version UNNEEDED, u32 hsm_wire_max_version UNNEEDED) +u8 *towire_hsmd_init(const tal_t *ctx UNNEEDED, const struct bip32_key_version *bip32_key_version UNNEEDED, const struct chainparams *chainparams UNNEEDED, const struct secret *hsm_encryption_key UNNEEDED, const struct privkey *dev_force_privkey UNNEEDED, const struct secret *dev_force_bip32_seed UNNEEDED, const struct secrets *dev_force_channel_secrets UNNEEDED, const struct sha256 *dev_force_channel_secrets_shaseed UNNEEDED, u32 hsm_wire_min_version UNNEEDED, u32 hsm_wire_max_version UNNEEDED, const struct tlv_hsmd_init_tlvs *tlvs UNNEEDED) { fprintf(stderr, "towire_hsmd_init called!\n"); abort(); } /* Generated stub for towire_hsmd_new_channel */ u8 *towire_hsmd_new_channel(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 dbid UNNEEDED)