From d393cda215568abed5792c1426ccbf3e79c402dd Mon Sep 17 00:00:00 2001 From: darosior Date: Thu, 3 Oct 2019 21:55:32 +0200 Subject: [PATCH] hsmd: encrypt hsm_secret if 'lightningd' pass an encryption key This splits maybe_create_hsm_secret() in two parts (either encrypted or in clear) for clarity, and adds an encryption detection in load_hsm(). There are actually three cases if an encryption key is passed: - There is no hsm_secret => just create it and store the encrypted seed - There is an encrypted hsm_secret => the provided key should be able to decrypt the seed, if the wrong key is passed libsodium will nicely error and hsmd will exit() to not throw a backtrace (using status_failed() as for other errors) at the face of an user who mistyped its password. - There is a non-encrypted hsm_secret => load the seed, delete the hsm_secret, create the hsm_secret, store the encrypted seed. --- hsmd/hsmd.c | 150 ++++++++++++++++++++++++++++++++++----- lightningd/hsm_control.c | 5 +- lightningd/lightningd.c | 6 ++ 3 files changed, 141 insertions(+), 20 deletions(-) diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index 1ef512654..a8f60a6f8 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -45,7 +45,7 @@ #include #include #include -#include +#include #include #include #include @@ -489,9 +489,55 @@ static void bitcoin_key(struct privkey *privkey, struct pubkey *pubkey, "BIP32 pubkey %u create failed", index); } +/*~ This encrypts the content of the secretstuff 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) +{ + crypto_secretstream_xchacha20poly1305_state crypto_state; + u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; + /* The cipher size is static with xchacha20poly1305 */ + u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; + + crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, header, + encryption_key->data); + crypto_secretstream_xchacha20poly1305_push(&crypto_state, cipher, + NULL, + secretstuff.hsm_secret.data, + sizeof(secretstuff.hsm_secret.data), + /* Additional data and tag */ + NULL, 0, 0); + if (!write_all(fd, header, sizeof(header))) { + unlink_noerr("hsm_secret"); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Writing header of encrypted secret: %s", strerror(errno)); + } + if (!write_all(fd, cipher, sizeof(cipher))) { + unlink_noerr("hsm_secret"); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Writing encrypted secret: %s", strerror(errno)); + } +} + +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, &secretstuff.hsm_secret, sizeof(secretstuff.hsm_secret))) { + /* ccan/noerr contains useful routines like this, which don't + * clobber errno, so we can use it in our error report. */ + unlink_noerr("hsm_secret"); + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "writing: %s", strerror(errno)); + } +} + /*~ We store our root secret in a "hsm_secret" file (like all of c-lightning, * we run in the user's .lightning directory). */ -static void maybe_create_new_hsm(void) +static void maybe_create_new_hsm(const struct secret *encryption_key, + bool random_hsm) { /*~ Note that this is opened for write-only, even though the permissions * are set to read-only. That's perfectly valid! */ @@ -501,22 +547,20 @@ static void maybe_create_new_hsm(void) if (errno == EEXIST) return; status_failed(STATUS_FAIL_INTERNAL_ERROR, - "creating: %s", strerror(errno)); + "creating: %s", strerror(errno)); } /*~ This is libsodium's cryptographic randomness routine: we assume * it's doing a good job. */ - randombytes_buf(&secretstuff.hsm_secret, sizeof(secretstuff.hsm_secret)); - /*~ 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, &secretstuff.hsm_secret, sizeof(secretstuff.hsm_secret))) { - /* ccan/noerr contains useful routines like this, which don't - * clobber errno, so we can use it in our error report. */ - unlink_noerr("hsm_secret"); - status_failed(STATUS_FAIL_INTERNAL_ERROR, - "writing: %s", strerror(errno)); - } + if (random_hsm) + randombytes_buf(&secretstuff.hsm_secret, sizeof(secretstuff.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); /*~ fsync (mostly!) ensures that the file has reached the disk. */ if (fsync(fd) != 0) { unlink_noerr("hsm_secret"); @@ -554,15 +598,67 @@ static void maybe_create_new_hsm(void) /*~ 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(void) +static void load_hsm(const struct secret *encryption_key) { + struct stat st; int fd = open("hsm_secret", O_RDONLY); if (fd < 0) status_failed(STATUS_FAIL_INTERNAL_ERROR, "opening: %s", strerror(errno)); - if (!read_all(fd, &secretstuff.hsm_secret, sizeof(secretstuff.hsm_secret))) + if (stat("hsm_secret", &st) != 0) status_failed(STATUS_FAIL_INTERNAL_ERROR, - "reading: %s", strerror(errno)); + "stating: %s", strerror(errno)); + + /* If the seed is stored in clear. */ + if (st.st_size <= 32) { + if (!read_all(fd, &secretstuff.hsm_secret, sizeof(secretstuff.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. */ + if (encryption_key && st.st_size > 32) { + crypto_secretstream_xchacha20poly1305_state crypto_state; + u8 header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; + /* The cipher size is static with xchacha20poly1305 */ + u8 cipher[sizeof(struct secret) + crypto_secretstream_xchacha20poly1305_ABYTES]; + + if (!read_all(fd, &header, crypto_secretstream_xchacha20poly1305_HEADERBYTES)) + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Reading xchacha20 header: %s", strerror(errno)); + if (!read_all(fd, cipher, sizeof(cipher))) + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Reading encrypted secret: %s", strerror(errno)); + if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, header, + encryption_key->data) != 0) + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Initializing the crypto state: %s", strerror(errno)); + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, + secretstuff.hsm_secret.data, + NULL, 0, cipher, sizeof(cipher), + NULL, 0) != 0) { + /* 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); + } + } + /* else { handled in hsm_control } */ close(fd); populate_secretstuff(); @@ -593,6 +689,15 @@ static struct io_plan *init_hsm(struct io_conn *conn, &hsm_encryption_key, &privkey, &seed, &secrets, &shaseed)) return bad_req(conn, c, msg_in); + /*~ The memory is actually copied in towire(), so lock the `hsm_secret` + * encryption key (new) memory again here. */ + if (hsm_encryption_key && sodium_mlock(hsm_encryption_key, + sizeof(hsm_encryption_key)) != 0) + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Could not lock memory for hsm_secret encryption key."); + /*~ Don't swap this. */ + sodium_mlock(secretstuff.hsm_secret.data, sizeof(secretstuff.hsm_secret.data)); + #if DEVELOPER dev_force_privkey = privkey; dev_force_bip32_seed = seed; @@ -603,8 +708,15 @@ static struct io_plan *init_hsm(struct io_conn *conn, /* Once we have read the init message we know which params the master * will use */ c->chainparams = chainparams; - maybe_create_new_hsm(); - load_hsm(); + maybe_create_new_hsm(hsm_encryption_key, true); + load_hsm(hsm_encryption_key); + + /*~ We don't need the hsm_secret encryption key anymore. + * Note that sodium_munlock() also zeroes the memory. */ + if (hsm_encryption_key) { + sodium_munlock(hsm_encryption_key, sizeof(*hsm_encryption_key)); + tal_free(hsm_encryption_key); + } /*~ We tell lightning our node id and (public) bip32 seed. */ node_key(NULL, &key); diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c index ca1fe6076..e051b19ef 100644 --- a/lightningd/hsm_control.c +++ b/lightningd/hsm_control.c @@ -106,6 +106,9 @@ void hsm_init(struct lightningd *ld) ld->wallet->bip32_base = tal(ld->wallet, struct ext_key); msg = wire_sync_read(tmpctx, ld->hsm_fd); if (!fromwire_hsm_init_reply(msg, - &ld->id, ld->wallet->bip32_base)) + &ld->id, ld->wallet->bip32_base)) { + if (ld->config.keypass) + errx(1, "Wrong password for encrypted hsm_secret."); errx(1, "HSM did not give init reply"); + } } diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 4ef9e5197..effb92dcf 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -76,6 +76,7 @@ #include #include #include +#include #include #include #include @@ -720,6 +721,11 @@ int main(int argc, char *argv[]) * Daemon Software Module. */ hsm_init(ld); + /*~ If hsm_secret is encrypted, we don't need its encryption key + * anymore. Note that sodium_munlock() also zeroes the memory.*/ + if (ld->config.keypass) + sodium_munlock(ld->config.keypass->data, sizeof(ld->config.keypass->data)); + /*~ Our default color and alias are derived from our node id, so we * can only set those now (if not set by config options). */ setup_color_and_alias(ld);