Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hsmtool (and hsm_secret encryption) refactorings #4307

Merged
merged 9 commits into from
Jan 6, 2021
1 change: 1 addition & 0 deletions common/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ COMMON_SRC_NOGEN := \
common/gossip_store.c \
common/hash_u5.c \
common/hmac.c \
common/hsm_encryption.c \
common/htlc_state.c \
common/htlc_trim.c \
common/htlc_tx.c \
Expand Down
115 changes: 115 additions & 0 deletions common/hsm_encryption.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#include <ccan/tal/str/str.h>
#include <common/hsm_encryption.h>
#include <sodium/utils.h>
#include <termios.h>

char *hsm_secret_encryption_key(const char *pass, struct secret *key)
{
u8 salt[16] = "c-lightning\0\0\0\0\0";

/* Don't swap the encryption key ! */
if (sodium_mlock(key->data, sizeof(key->data)) != 0)
return "Could not lock hsm_secret encryption key memory.";

/* Check bounds. */
if (strlen(pass) < crypto_pwhash_argon2id_PASSWD_MIN)
return "Password too short to be able to derive a key from it.";
if (strlen(pass) > crypto_pwhash_argon2id_PASSWD_MAX)
return "Password too long to be able to derive a key from it.";

/* Now derive the key. */
if (crypto_pwhash(key->data, sizeof(key->data), pass, strlen(pass), 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 "Could not derive a key from the password.";

return NULL;
}

bool encrypt_hsm_secret(const struct secret *encryption_key,
const struct secret *hsm_secret,
struct encrypted_hsm_secret *output)
{
crypto_secretstream_xchacha20poly1305_state crypto_state;

if (crypto_secretstream_xchacha20poly1305_init_push(&crypto_state, output->data,
encryption_key->data) != 0)
return false;
if (crypto_secretstream_xchacha20poly1305_push(&crypto_state,
output->data + HS_HEADER_LEN,
NULL, hsm_secret->data,
sizeof(hsm_secret->data),
/* Additional data and tag */
NULL, 0, 0))
return false;

return true;
}

bool decrypt_hsm_secret(const struct secret *encryption_key,
const struct encrypted_hsm_secret *cipher,
struct secret *output)
{
crypto_secretstream_xchacha20poly1305_state crypto_state;

/* The header part */
if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, cipher->data,
encryption_key->data) != 0)
return false;
/* The ciphertext part */
if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, output->data,
NULL, 0,
cipher->data + HS_HEADER_LEN,
HS_CIPHERTEXT_LEN,
NULL, 0) != 0)
return false;

return true;
}

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

char *read_stdin_pass(char **reason)
{
struct termios current_term, temp_term;
char *passwd = NULL;
size_t passwd_size = 0;

/* Set a temporary term, same as current but with ECHO disabled. */
if (tcgetattr(fileno(stdin), &current_term) != 0) {
*reason = "Could not get current terminal options.";
return NULL;
}
temp_term = current_term;
temp_term.c_lflag &= ~ECHO;
if (tcsetattr(fileno(stdin), TCSAFLUSH, &temp_term) != 0) {
*reason = "Could not disable pass echoing.";
return NULL;
}

/* Read the password, do not take the newline character into account. */
if (getline(&passwd, &passwd_size, stdin) < 0) {
*reason = "Could not read pass from stdin.";
return NULL;
}
if (passwd[strlen(passwd) - 1] == '\n')
passwd[strlen(passwd) - 1] = '\0';

/* Restore the original terminal */
if (tcsetattr(fileno(stdin), TCSAFLUSH, &current_term) != 0) {
*reason = "Could not restore terminal options.";
free(passwd);
return NULL;
}

return passwd;
}
64 changes: 64 additions & 0 deletions common/hsm_encryption.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#ifndef LIGHTNING_COMMON_HSM_ENCRYPTION_H
#define LIGHTNING_COMMON_HSM_ENCRYPTION_H
#include "config.h"
#include <bitcoin/privkey.h>
#include <ccan/short_types/short_types.h>
#include <ccan/tal/tal.h>
#include <sodium.h>

/* 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)

struct encrypted_hsm_secret {
u8 data[ENCRYPTED_HSM_SECRET_LEN];
};

/** Derive the hsm_secret encryption key from a passphrase.
* @pass: the passphrase string.
* @encryption_key: the output key derived from the passphrase.
*
* On success, NULL is returned. On error, a human-readable error is.
*/
char *hsm_secret_encryption_key(const char *pass, struct secret *encryption_key);

/** Encrypt the hsm_secret using a previously derived encryption key.
* @encryption_key: the key derived from the passphrase.
* @hsm_secret: the plaintext hsm_secret to encrypt.
* @output: the resulting encrypted hsm_secret.
*
* Return false on encryption failure.
*/
bool encrypt_hsm_secret(const struct secret *encryption_key,
const struct secret *hsm_secret,
struct encrypted_hsm_secret *output);

/** Decrypt the hsm_secret using a previously derived encryption key.
* @encryption_key: the key derived from the passphrase.
* @cipher: the encrypted hsm_secret to decrypt.
* @output: the resulting hsm_secret.
*
* Return false on decryption failure.
*/
bool decrypt_hsm_secret(const struct secret *encryption_key,
const struct encrypted_hsm_secret *cipher,
struct secret *output);

/** Unlock and zeroize the encryption key memory after use.
* @key: the encryption key. If taken, it will be tal_free'd
*/
void discard_key(struct secret *key TAKES);

/** Read hsm_secret encryption pass from stdin, disabling echoing.
* @reason: if NULL is returned, will point to the human-readable error.
*
* Caller must free the string as it does tal-reallocate getline's output.
*/
char *read_stdin_pass(char **reason);

#endif /* LIGHTNING_COMMON_HSM_ENCRYPTION_H */
1 change: 1 addition & 0 deletions hsmd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ HSMD_COMMON_OBJS := \
common/derive_basepoints.o \
common/status_wiregen.o \
common/hash_u5.o \
common/hsm_encryption.o \
common/key_derive.o \
common/memleak.o \
common/msg_queue.o \
Expand Down
72 changes: 26 additions & 46 deletions hsmd/hsmd.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <common/daemon_conn.h>
#include <common/derive_basepoints.h>
#include <common/hash_u5.h>
#include <common/hsm_encryption.h>
#include <common/key_derive.h>
#include <common/memleak.h>
#include <common/node_id.h>
Expand Down Expand Up @@ -562,28 +563,16 @@ static void bitcoin_key(struct privkey *privkey, struct pubkey *pubkey,
*/
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");
struct encrypted_hsm_secret cipher;

if (!encrypt_hsm_secret(encryption_key, &secretstuff.hsm_secret,
&cipher))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Writing header of encrypted secret: %s", strerror(errno));
}
if (!write_all(fd, cipher, sizeof(cipher))) {
"Encrypting hsm_secret");
if (!write_all(fd, cipher.data, ENCRYPTED_HSM_SECRET_LEN)) {
unlink_noerr("hsm_secret");
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Writing encrypted secret: %s", strerror(errno));
"Writing encrypted hsm_secret: %s", strerror(errno));
}
}

Expand Down Expand Up @@ -677,7 +666,7 @@ static void load_hsm(const struct secret *encryption_key)
"stating: %s", strerror(errno));

/* If the seed is stored in clear. */
if (st.st_size <= 32) {
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));
Expand All @@ -697,35 +686,29 @@ static void load_hsm(const struct secret *encryption_key)
"opening: %s", strerror(errno));
}
}
/*~ If an encryption key was passed and the `hsm_secret` is stored
/* 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];
else if (st.st_size == ENCRYPTED_HSM_SECRET_LEN) {
struct encrypted_hsm_secret encrypted_secret;

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)
/* hsm_control must have checked it! */
assert(encryption_key);

if (!read_all(fd, encrypted_secret.data, ENCRYPTED_HSM_SECRET_LEN))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"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) {
"Reading encrypted hsm_secret: %s", strerror(errno));
if (!decrypt_hsm_secret(encryption_key, &encrypted_secret,
&secretstuff.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);
}
}
/* else { handled in hsm_control } */
else
status_failed(STATUS_FAIL_INTERNAL_ERROR, "Invalid hsm_secret, "
"no plaintext nor encrypted"
" seed.");
close(fd);

populate_secretstuff();
Expand Down Expand Up @@ -779,12 +762,9 @@ static struct io_plan *init_hsm(struct io_conn *conn,
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 don't need the hsm_secret encryption key anymore. */
if (hsm_encryption_key)
discard_key(take(hsm_encryption_key));

/*~ We tell lightning our node id and (public) bip32 seed. */
node_key(NULL, &key);
Expand Down
1 change: 1 addition & 0 deletions lightningd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ LIGHTNINGD_COMMON_OBJS := \
common/gossip_rcvd_filter.o \
common/hash_u5.o \
common/hmac.o \
common/hsm_encryption.o \
common/htlc_state.o \
common/htlc_trim.o \
common/htlc_wire.o \
Expand Down
4 changes: 3 additions & 1 deletion lightningd/hsm_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <ccan/io/io.h>
#include <ccan/take/take.h>
#include <common/ecdh.h>
#include <common/hsm_encryption.h>
#include <common/json.h>
#include <common/json_helpers.h>
#include <common/jsonrpc_errors.h>
Expand Down Expand Up @@ -106,7 +107,8 @@ struct ext_key *hsm_init(struct lightningd *ld)
* actual secret. */
if (!ld->config.keypass) {
struct stat st;
if (stat("hsm_secret", &st) == 0 && st.st_size > 32)
if (stat("hsm_secret", &st) == 0 &&
st.st_size == ENCRYPTED_HSM_SECRET_LEN)
errx(1, "hsm_secret is encrypted, you need to pass the "
"--encrypted-hsm startup option.");
}
Expand Down
3 changes: 2 additions & 1 deletion lightningd/lightningd.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#include <common/daemon.h>
#include <common/ecdh_hsmd.h>
#include <common/features.h>
#include <common/hsm_encryption.h>
#include <common/memleak.h>
#include <common/timeout.h>
#include <common/utils.h>
Expand Down Expand Up @@ -878,7 +879,7 @@ int main(int argc, char *argv[])
/*~ 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));
discard_key(take(ld->config.keypass));

/*~ Our default color and alias are derived from our node id, so we
* can only set those now (if not set by config options). */
Expand Down
Loading