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.
This commit is contained in:
darosior 2019-10-03 21:55:32 +02:00 committed by neil saitug
parent 62896566d0
commit d393cda215
3 changed files with 141 additions and 20 deletions

View File

@ -45,7 +45,7 @@
#include <hsmd/gen_hsm_wire.h>
#include <inttypes.h>
#include <secp256k1_ecdh.h>
#include <sodium/randombytes.h>
#include <sodium.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
@ -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);

View File

@ -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");
}
}

View File

@ -76,6 +76,7 @@
#include <lightningd/options.h>
#include <onchaind/onchain_wire.h>
#include <signal.h>
#include <sodium.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
@ -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);