diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md index 3da0727a7..02ed83e82 100644 --- a/doc/lightning-hsmtool.8.md +++ b/doc/lightning-hsmtool.8.md @@ -51,6 +51,9 @@ Specify *password* if the `hsm_secret` is encrypted. **generatehsm** *hsm\_secret\_path* Generates a new hsm_secret using BIP39. +**checkhsm** *hsm\_secret\_path* +Checks that hsm_secret matchs a BIP39 pass phrase. + **dumponchaindescriptors** *hsm_secret* \[*password*\] \[*network*\] Dump output descriptors for our onchain wallet. The descriptors can be used by external services to be able to generate diff --git a/tests/test_wallet.py b/tests/test_wallet.py index ddee4c3d8..ac5fd0e7d 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1218,7 +1218,6 @@ def test_hsmtool_dump_descriptors(node_factory, bitcoind): assert len(bitcoind.rpc.listunspent(1, 1, [addr])) == 1 -@unittest.skipIf(VALGRIND, "It does not play well with prompt and key derivation.") def test_hsmtool_generatehsm(node_factory): l1 = node_factory.get_node(start=False) hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, @@ -1242,9 +1241,48 @@ def test_hsmtool_generatehsm(node_factory): "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.proc.wait(WAIT_TIMEOUT) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 hsmtool.is_in_log(r"New hsm_secret file created") + # Check should pass. + 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")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0 + hsmtool.is_in_log(r"OK") + + # Wrong mnemonic will fail. + 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, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n".encode("utf-8")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 + hsmtool.is_in_log(r"resulting hsm_secret did not match") + + # Wrong passphrase will fail. + 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")) + assert hsmtool.proc.wait(WAIT_TIMEOUT) == 5 + 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' diff --git a/tools/hsmtool.c b/tools/hsmtool.c index e6ae1e36c..226046365 100644 --- a/tools/hsmtool.c +++ b/tools/hsmtool.c @@ -39,6 +39,7 @@ static void show_usage(const char *progname) printf(" - guesstoremote " "\n"); printf(" - generatehsm \n"); + printf(" - checkhsm \n"); printf(" - dumponchaindescriptors [network]\n"); exit(0); } @@ -595,6 +596,60 @@ static int dumponchaindescriptors(const char *hsm_secret_path, const char *old_p return 0; } +static int check_hsm(const char *hsm_secret_path) +{ + char mnemonic[BIP39_WORDLIST_LEN]; + struct secret hsm_secret; + u8 bip32_seed[BIP39_SEED_LEN_512]; + size_t bip32_seed_len; + int exit_code; + char *passphrase, *err; + + /* This checks the file existence, too. */ + if (hsm_secret_is_encrypted(hsm_secret_path)) { + char *passwd; + + 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); + + if (sodium_init() == -1) + errx(ERROR_LIBSODIUM, + "Could not initialize libsodium. Not enough entropy ?"); + + get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd); + /* Once the encryption key derived, we don't need it anymore. */ + free(passwd); + } else + get_hsm_secret(&hsm_secret, hsm_secret_path); + + 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; + } + + read_mnemonic(mnemonic); + 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) + errx(ERROR_KEYDERIV, "resulting hsm_secret did not match"); + + printf("OK\n"); + return 0; +} + int main(int argc, char *argv[]) { const char *method; @@ -682,5 +737,11 @@ int main(int argc, char *argv[]) return dumponchaindescriptors(argv[2], NULL, is_testnet); } + if (streq(method, "checkhsm")) { + if (argc < 3) + show_usage(argv[0]); + return check_hsm(argv[2]); + } + show_usage(argv[0]); }