Skip to content

Commit e261301

Browse files
committed
exposesecret: new plugin and command to get hsm_secret.
Being able to back up the hsm_secret is critical, but you cannot do this through a UI, because of course we do not allow such access. People have lost funds because they didn't back up. This allows access to the hsm_secret if you use a password set in the config file. (If it's not set, the command does not work). This is a compromise, of course. Changelog-Added: `exposesecret` command for encouraging hsm_secret backups. Signed-off-by: Rusty Russell <[email protected]>
1 parent ab6f405 commit e261301

File tree

8 files changed

+369
-0
lines changed

8 files changed

+369
-0
lines changed

contrib/msggen/msggen/schema.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12782,6 +12782,64 @@
1278212782
}
1278312783
]
1278412784
},
12785+
"lightning-exposesecret.json": {
12786+
"$schema": "../rpc-schema-draft.json",
12787+
"type": "object",
12788+
"additionalProperties": false,
12789+
"rpc": "exposesecret",
12790+
"title": "Command for extracting the hsm_secret file for backup",
12791+
"description": [
12792+
"The **exposesecret** RPC command allows you to read the HSM secret, and does not work with encrypted hsm secrets. It only operates if the `exposesecret-passphrase` has been set in the configuration."
12793+
],
12794+
"request": {
12795+
"required": [
12796+
"passphrase"
12797+
],
12798+
"properties": {
12799+
"passphrase": {
12800+
"type": "string",
12801+
"description": [
12802+
"The passphrase, which must match the `exposesecret-passphrase` configuration parameter."
12803+
]
12804+
},
12805+
"identifier": {
12806+
"type": "string",
12807+
"description": [
12808+
"A four-character, valid lowercase bech32 string (not 1, i, o or b) to use in the resulting BIP-93 output. If not specified, this is generated from the node alias."
12809+
]
12810+
}
12811+
}
12812+
},
12813+
"response": {
12814+
"required": [
12815+
"identifier",
12816+
"codex32"
12817+
],
12818+
"properties": {
12819+
"identifier": {
12820+
"type": "string",
12821+
"description": [
12822+
"The four-character identifier used in the codex32 output. Redundant, but presented separately for clarity."
12823+
]
12824+
},
12825+
"codex32": {
12826+
"type": "string",
12827+
"description": [
12828+
"The full codex32-encoded (i.e. BIP-93 encoded) HSM secret."
12829+
]
12830+
}
12831+
}
12832+
},
12833+
"author": [
12834+
"Rusty Russell <<[email protected]>> is mainly responsible."
12835+
],
12836+
"see_also": [
12837+
"lightning-hsmtool(8)"
12838+
],
12839+
"resources": [
12840+
"Main web site: <https://github.com/ElementsProject/lightning>"
12841+
]
12842+
},
1278512843
"lightning-feerates.json": {
1278612844
"$schema": "../rpc-schema-draft.json",
1278712845
"type": "object",

doc/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \
5555
doc/lightning-disableoffer.7 \
5656
doc/lightning-disconnect.7 \
5757
doc/lightning-emergencyrecover.7 \
58+
doc/lightning-exposesecret.7 \
5859
doc/lightning-feerates.7 \
5960
doc/lightning-fetchinvoice.7 \
6061
doc/lightning-fundchannel_cancel.7 \

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Core Lightning Documentation
6464
lightning-disableoffer <lightning-disableoffer.7.md>
6565
lightning-disconnect <lightning-disconnect.7.md>
6666
lightning-emergencyrecover <lightning-emergencyrecover.7.md>
67+
lightning-exposesecret <lightning-exposesecret.7.md>
6768
lightning-feerates <lightning-feerates.7.md>
6869
lightning-fetchinvoice <lightning-fetchinvoice.7.md>
6970
lightning-fundchannel <lightning-fundchannel.7.md>

doc/lightningd-config.5.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,11 @@ authenticate to the Tor control port.
702702

703703
Defines the path for WSS cert & key. Default path is same as RPC file path to utilize gRPC/clnrest's client certificate. If it is missing at the configured location, new identity (`client.pem` and `client-key.pem`) will be generated.
704704

705+
* **exposesecret-passphrase**=*passphrase* [plugin `exposesecret`]
706+
707+
Defines a passphrase which will let users extract the `hsm_secret` using the `exposesecret` command. If this is not set, the `exposesecret` command always fails.
708+
709+
705710
### Lightning Plugins
706711

707712
lightningd(8) supports plugins, which offer additional configuration
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"$schema": "../rpc-schema-draft.json",
3+
"type": "object",
4+
"additionalProperties": false,
5+
"rpc": "exposesecret",
6+
"title": "Command for extracting the hsm_secret file for backup",
7+
"description": [
8+
"The **exposesecret** RPC command allows you to read the HSM secret, and does not work with encrypted hsm secrets. It only operates if the `exposesecret-passphrase` has been set in the configuration."
9+
],
10+
"request": {
11+
"required": [
12+
"passphrase"
13+
],
14+
"properties": {
15+
"passphrase": {
16+
"type": "string",
17+
"description": [
18+
"The passphrase, which must match the `exposesecret-passphrase` configuration parameter."
19+
]
20+
},
21+
"identifier": {
22+
"type": "string",
23+
"description": [
24+
"A four-character, valid lowercase bech32 string (not 1, i, o or b) to use in the resulting BIP-93 output. If not specified, this is generated from the node alias."
25+
]
26+
}
27+
}
28+
},
29+
"response": {
30+
"required": [
31+
"identifier",
32+
"codex32"
33+
],
34+
"properties": {
35+
"identifier": {
36+
"type": "string",
37+
"description": [
38+
"The four-character identifier used in the codex32 output. Redundant, but presented separately for clarity."
39+
]
40+
},
41+
"codex32": {
42+
"type": "string",
43+
"description": [
44+
"The full codex32-encoded (i.e. BIP-93 encoded) HSM secret."
45+
]
46+
}
47+
}
48+
},
49+
"author": [
50+
"Rusty Russell <<[email protected]>> is mainly responsible."
51+
],
52+
"see_also": [
53+
"lightning-hsmtool(8)"
54+
],
55+
"resources": [
56+
"Main web site: <https://github.com/ElementsProject/lightning>"
57+
]
58+
}

plugins/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ PLUGIN_SQL_SRC := plugins/sql.c
4545
PLUGIN_SQL_HEADER :=
4646
PLUGIN_SQL_OBJS := $(PLUGIN_SQL_SRC:.c=.o)
4747

48+
PLUGIN_EXPOSESECRET_SRC := plugins/exposesecret.c
49+
PLUGIN_EXPOSESECRET_HEADER :=
50+
PLUGIN_EXPOSESECRET_OBJS := $(PLUGIN_EXPOSESECRET_SRC:.c=.o)
51+
4852
PLUGIN_SPENDER_SRC := \
4953
plugins/spender/fundchannel.c \
5054
plugins/spender/main.c \
@@ -81,6 +85,7 @@ PLUGIN_ALL_SRC := \
8185
$(PLUGIN_COMMANDO_SRC) \
8286
$(PLUGIN_FUNDER_SRC) \
8387
$(PLUGIN_TOPOLOGY_SRC) \
88+
$(PLUGIN_EXPOSESECRET_SRC) \
8489
$(PLUGIN_KEYSEND_SRC) \
8590
$(PLUGIN_TXPREPARE_SRC) \
8691
$(PLUGIN_LIB_SRC) \
@@ -106,6 +111,7 @@ C_PLUGINS := \
106111
plugins/commando \
107112
plugins/funder \
108113
plugins/topology \
114+
plugins/exposesecret \
109115
plugins/keysend \
110116
plugins/offers \
111117
plugins/pay \
@@ -221,6 +227,8 @@ plugins/topology: common/route.o common/dijkstra.o common/gossmap.o common/scidd
221227

222228
plugins/txprepare: $(PLUGIN_TXPREPARE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS)
223229

230+
plugins/exposesecret: $(PLUGIN_EXPOSESECRET_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) common/hsm_encryption.o common/codex32.o
231+
224232
plugins/bcli: $(PLUGIN_BCLI_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS)
225233

226234
plugins/keysend: wire/tlvstream.o wire/onion_wiregen.o $(PLUGIN_KEYSEND_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_PAY_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) common/gossmap.o common/fp16.o common/route.o common/dijkstra.o common/blindedpay.o common/blindedpath.o common/hmac.o common/blinding.o common/onion_encode.o common/gossmods_listpeerchannels.o common/sciddir_or_pubkey.o

plugins/exposesecret.c

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#include "config.h"
2+
#include <bitcoin/privkey.h>
3+
#include <ccan/array_size/array_size.h>
4+
#include <ccan/crypto/hkdf_sha256/hkdf_sha256.h>
5+
#include <ccan/crypto/sha256/sha256.h>
6+
#include <ccan/tal/grab_file/grab_file.h>
7+
#include <ccan/tal/str/str.h>
8+
#include <common/bech32.h>
9+
#include <common/codex32.h>
10+
#include <common/json_param.h>
11+
#include <common/json_stream.h>
12+
#include <errno.h>
13+
#include <plugins/libplugin.h>
14+
15+
/* Information this plugin wants to keep. */
16+
struct exposesecret {
17+
char *exposure_passphrase;
18+
struct pubkey our_node_id;
19+
const char *our_node_alias;
20+
};
21+
22+
static struct exposesecret *exposesecret_data(struct plugin *plugin)
23+
{
24+
return plugin_get_data(plugin, struct exposesecret);
25+
}
26+
27+
/* Don't let compiler do clever things, which would allow the caller
28+
* to measure time, and figure out how much of the passphrase matched! */
29+
static bool compare_passphrases(const char *a, const char *b)
30+
{
31+
struct sha256 a_sha, b_sha;
32+
33+
/* Technically, this gives information about passphrase length, but
34+
* they can also just brute force it if it is small, so this doesn't
35+
* add much! Also, hashing messes with timing quite a bit. */
36+
sha256(&a_sha, a, strlen(a));
37+
sha256(&b_sha, b, strlen(b));
38+
39+
return sha256_eq(&a_sha, &b_sha);
40+
}
41+
42+
static struct command_result *json_exposesecret(struct command *cmd,
43+
const char *buffer,
44+
const jsmntok_t *params)
45+
{
46+
const struct exposesecret *exposesecret = exposesecret_data(cmd->plugin);
47+
struct json_stream *js;
48+
u8 *contents;
49+
const char *id, *passphrase, *err;
50+
struct secret hsm_secret;
51+
struct privkey node_privkey;
52+
struct pubkey node_id;
53+
char *bip93;
54+
u32 salt = 0;
55+
56+
if (!param_check(cmd, buffer, params,
57+
p_req("passphrase", param_string, &passphrase),
58+
p_opt("identifier", param_string, &id),
59+
NULL))
60+
return command_param_failed();
61+
62+
if (!exposesecret->exposure_passphrase)
63+
return command_fail(cmd, LIGHTNINGD, "exposesecrets-passphrase is not set");
64+
65+
/* Technically, this could become a timing oracle. */
66+
if (!compare_passphrases(exposesecret->exposure_passphrase, passphrase))
67+
return command_fail(cmd, LIGHTNINGD, "passphrase does not match exposesecrets-passphrase");
68+
69+
contents = grab_file(tmpctx, "hsm_secret");
70+
if (!contents)
71+
return command_fail(cmd, LIGHTNINGD, "Could not open hsm_secret: %s", strerror(errno));
72+
73+
/* grab_file adds a \0 byte at the end for convenience */
74+
if (tal_bytelen(contents) == sizeof(hsm_secret) + 1) {
75+
memcpy(&hsm_secret, contents, sizeof(hsm_secret));
76+
} else {
77+
return command_fail(cmd, LIGHTNINGD, "Not a valid hsm_secret file? Bad length (maybe encrypted?)");
78+
}
79+
80+
/* Before we expose it, check it's correct! */
81+
hkdf_sha256(&node_privkey, sizeof(node_privkey),
82+
&salt, sizeof(salt),
83+
&hsm_secret,
84+
sizeof(hsm_secret),
85+
"nodeid", 6);
86+
87+
/* Should not happen! */
88+
if (!pubkey_from_privkey(&node_privkey, &node_id))
89+
return command_fail(cmd, LIGHTNINGD, "Invalid private key?");
90+
91+
if (!pubkey_eq(&node_id, &exposesecret->our_node_id))
92+
return command_fail(cmd, LIGHTNINGD, "This hsm_secret is not for the current node");
93+
94+
/* If they didn't give an identifier, we make an appropriate one! */
95+
if (!id) {
96+
size_t off = 0;
97+
/* If we run out of alias, use x. */
98+
char idstr[] = "xxxx";
99+
100+
for (size_t i = 0; idstr[off]; i++) {
101+
unsigned char c = exposesecret->our_node_alias[i];
102+
if (c == 0)
103+
break;
104+
if (c >= sizeof(bech32_charset_rev))
105+
continue;
106+
/* Convert to lower case */
107+
c = tolower(c);
108+
/* Must be a valid bech32 char now */
109+
if (bech32_charset_rev[c] == -1)
110+
continue;
111+
idstr[off++] = c;
112+
}
113+
114+
id = tal_strdup(cmd, idstr);
115+
}
116+
117+
/* This also cannot fail! */
118+
err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93);
119+
if (err)
120+
return command_fail(cmd, LIGHTNINGD, "Unexpected failure encoding hsm_secret: %s", err);
121+
122+
/* If we're just checking, stop */
123+
if (command_check_only(cmd))
124+
return command_check_done(cmd);
125+
126+
js = jsonrpc_stream_success(cmd);
127+
json_add_string(js, "identifier", id);
128+
json_add_string(js, "codex32", bip93);
129+
return command_finished(cmd, js);
130+
}
131+
132+
static const char *init(struct plugin *plugin,
133+
const char *buf UNUSED, const jsmntok_t *config UNUSED)
134+
{
135+
struct exposesecret *exposesecret = exposesecret_data(plugin);
136+
rpc_scan(plugin, "getinfo",
137+
take(json_out_obj(NULL, NULL, NULL)),
138+
"{id:%,alias:%}",
139+
JSON_SCAN(json_to_pubkey, &exposesecret->our_node_id),
140+
JSON_SCAN_TAL(exposesecret, json_strdup, &exposesecret->our_node_alias));
141+
return NULL;
142+
}
143+
144+
static const struct plugin_command commands[] = {
145+
{
146+
"exposesecret",
147+
json_exposesecret,
148+
}
149+
};
150+
151+
int main(int argc, char *argv[])
152+
{
153+
setup_locale();
154+
155+
struct exposesecret *exposesecret = talz(NULL, struct exposesecret);
156+
plugin_main(argv, init, take(exposesecret),
157+
PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands),
158+
NULL, 0, NULL, 0, NULL, 0,
159+
plugin_option("exposesecret-passphrase", "string",
160+
"Enable exposesecret command to allow HSM Secret backup, with this passphrase",
161+
charp_option, NULL, &exposesecret->exposure_passphrase),
162+
NULL);
163+
}

0 commit comments

Comments
 (0)