lightning-hsmtool: support extracting the mnemonic from hsm_secret.
What changed, and why it matters
This commit updates the Core Lightning wallet recovery tool so it can extract the correct backup phrase (mnemonic) used by newer nodes. The old command only produced a codex32 string, which would cause v25.12+ nodes to derive the wrong keys and potentially be unable to find or spend funds during recovery. There is no vulnerability being fixed; it is a usability and correctness improvement for wallet backups.
No urgent action. Operators should prefer `lightning-hsmtool getsecret` for backups on v25.12+ nodes and update any scripts still using `getcodexsecret`.
Security signals we found
Incorrect backup/recovery format could lead to inaccessible funds
Deprecation of legacy command that does not work with modern secret storage
Tool now rejects use of old command on mnemonic-format secrets
Evidence from the diff
The patch adds a getsecret subcommand to lightning-hsmtool and deprecates getcodexsecret. For modern hsm_secret files that store a BIP-39 mnemonic, getsecret prints the mnemonic directly. For legacy plain hsm_secret files, it still produces a BIP-93/codex32 string when an id is supplied. The old getcodexsecret command is retained as a deprecated alias but now refuses to run against mnemonic-style secrets. The change prevents users from recovering v25.12+ nodes with an incompatible secret format that would lead to incorrect key derivation.
Changed components
tools/lightning-hsmtool.cdoc/lightning-hsmtool.8.mddoc/beginners-guide/backup.mddoc/developers-guide/deprecated-features.mdtests/test_wallet.pyInspect captured patch +44 / −16
diff --git a/doc/beginners-guide/backup.md b/doc/beginners-guide/backup.md
index 9811b9d2..09a24c5a 100644
--- a/doc/beginners-guide/backup.md
+++ b/doc/beginners-guide/backup.md
@@ -63,11 +63,12 @@ chmod 0400 hsm_secret
```
-#### Codex32 Format
+#### Readable Format
-Run `tools/lightning-hsmtool getcodexsecret <hsm/secret/path> <id>` to get the `hsm_secret` in codex32 format.
+Run `tools/lightning-hsmtool getsecret <hsm/secret/path>` to get the `hsm_secret` mnemonic (12 words). For older
+nodes, you will get a codex32 string instead, and must supply a four-letter id to attach to it, like so:
-Example `tools/lightning-hsmtool getcodexsecret ~/.lightning/bitcoin/hsm_secret adt0`.
+Example `tools/lightning-hsmtool getsecret ~/.lightning/bitcoin/hsm_secret adt0`.
`hsm/secret/path` in the above command is `$LIGHTNINGDIR/hsm_secret`, and
`id` is any 4 character string used to identify this secret. It **cannot** contain `i`, `o`, or `b`, but **can** contain all digits except `1`.
diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md
index 5da91a55..f8cd848a 100644
--- a/doc/developers-guide/deprecated-features.md
+++ b/doc/developers-guide/deprecated-features.md
@@ -26,7 +26,7 @@ privacy:
| encrypted_hsm | Config | v25.12 | v26.12 | `hsm-passphrase` is a name which also makes sense for modern hsm_secrets which use BIP 39 |
| newaddr.addresstype.defaultbech32 | Parameter | v25.12 | v26.12 | Use `p2tr` in the response (present since v23.08 if `addresstype` is `p2tr`, and always present since v24.12). |
| channel_state_changed.null_message | Notification Field | v25.12 | v26.12 | In channel_state_changed notification, `message` will be missing instead of `null` |
-
+| hsmtool.getcodexsecret | Command | v25.12.1 | v26.12 | Doesn't work on nodes using mnemonic secrets (v25.12 or later). Use `getsecret` instead. |
Inevitably there are features which need to change: either to be generalized, or removed when they can no longer be supported.
Types of deprecation:
diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md
index e198d6cf..a9f6610f 100644
--- a/doc/lightning-hsmtool.8.md
+++ b/doc/lightning-hsmtool.8.md
@@ -88,8 +88,11 @@ the last parameter. By default, mainnet-encoded keys are generated.
This produces the same results as lightning-commando-rune(7) on a fresh node.
You will still need to create a rune once the node starts, if you want commando to work (as it is only activated once it has generated one).
+**getsecret** *hsm\_secret\_path* [*id*]
+ Extract the secret from the `hsm_secret` file and print it out, for use with `--recover`. This is usually a 12-word mnemonic, but for pre-v25.12 nodes, a 4-character `id` is needed to generate a BIP-93 formatted HSM secret (e.g. `ad00`): it cannot contain `i`, `o`, or `b`, but can contain digits except `1`.
+
**getcodexsecret** *hsm\_secret\_path* *id*
- Print out the BIP-93 formatted HSM secret, for use with `--recover`. The `id` is any 4 character string you can use to identify this secret (e.g. `ad00`): it cannot contain `i`, `o`, or `b`, but can contain digits except `1`.
+ Deprecated alias for getsecret: only works on older (pre-v25.12) nodes.
**getemergencyrecover** *emergency.recover\_path*
Print out the bech32 encoded emergency.recover file.
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index a9a86f0d..40282125 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -1649,7 +1649,7 @@ def test_hsmtool_all_commands_work_with_mnemonic_formats(node_factory):
# Test various commands work with mnemonic format
test_commands = [
(["getnodeid", hsm_path], "03653e90c1ce4660fd8505dd6d643356e93cfe202af109d382787639dd5890e87d"),
- (["getcodexsecret", hsm_path, "test"], "cl10testst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqsghuxusm6m6azq"),
+ (["getsecret", hsm_path], "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"),
(["makerune", hsm_path], "6VkrWMI2hm2a2UTkg-EyUrrBJN0RcuPB80I1pCVkTD89MA=="),
(["dumponchaindescriptors", hsm_path], "wpkh(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*)#2jtshmk0\nsh(wpkh(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*))#u6am4was\ntr(xpub661MyMwAqRbcG9kjo3mdWQuSDbtdJzsd3K2mvifyeUMF3GhLcBAfELqjuxCvxUkYqQVe6rJ9SzmpipoUedb5MD79MJaLL8RME2A3J3Fw6Zd/0/0/*)#v9hf4756"),
]
diff --git a/tools/lightning-hsmtool.c b/tools/lightning-hsmtool.c
index a3f487a1..ca5bf542 100644
--- a/tools/lightning-hsmtool.c
+++ b/tools/lightning-hsmtool.c
@@ -56,7 +56,7 @@ static void show_usage(const char *progname)
printf(" - checkhsm <path/to/new/hsm_secret>\n");
printf(" - dumponchaindescriptors [--show-secrets] <path/to/hsm_secret> [network]\n");
printf(" - makerune <path/to/hsm_secret>\n");
- printf(" - getcodexsecret <path/to/hsm_secret> <id>\n");
+ printf(" - getsecret <path/to/hsm_secret> [<id>]\n");
printf(" - getemergencyrecover <path/to/emergency.recover>\n");
printf(" - getnodeid <path/to/hsm_secret>\n");
exit(0);
@@ -269,20 +269,40 @@ static void get_channel_seed(struct secret *channel_seed, const struct node_id *
info, strlen(info));
}
-static void print_codexsecret(const char *hsm_secret_path, const char *id)
+static void print_secret(const char *hsm_secret_path, const char *id, bool must_be_oldstyle)
{
struct secret hsm_secret;
char *bip93;
const char *err;
struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path);
- /* Extract first 32 bytes for legacy compatibility */
- memcpy(hsm_secret.data, hsms->secret_data, 32);
- err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93);
- if (err)
- errx(ERROR_USAGE, "%s", err);
-
- printf("%s\n", bip93);
+ switch (hsms->type) {
+ case HSM_SECRET_ENCRYPTED:
+ errx(ERROR_USAGE, "Encrypted hsm_secret");
+ case HSM_SECRET_MNEMONIC_NO_PASS:
+ if (must_be_oldstyle)
+ errx(ERROR_USAGE, "Cannot use getcodexsecret with modern nodes: use getsecret");
+ printf("%s\n", hsms->mnemonic);
+ return;
+ case HSM_SECRET_MNEMONIC_WITH_PASS:
+ errx(ERROR_USAGE, "hsm_secret with passphrase");
+ case HSM_SECRET_PLAIN:
+ if (id == NULL)
+ errx(ERROR_USAGE, "Must set 'id' for a codex32 secret");
+ /* Extract first 32 bytes for legacy compatibility */
+ memcpy(hsm_secret.data, hsms->secret_data, 32);
+
+ err = codex32_secret_encode(tmpctx, "cl", id, 0, hsm_secret.data, 32, &bip93);
+ if (err)
+ errx(ERROR_USAGE, "%s", err);
+
+ printf("%s\n", bip93);
+ return;
+ case HSM_SECRET_INVALID:
+ break;
+ }
+ /* Never happens. */
+ abort();
}
static void print_emergencyrecover(const char *emer_rec_path)
@@ -745,10 +765,14 @@ int main(int argc, char *argv[])
if (argc < 3)
show_usage(argv[0]);
make_rune(argv[2]);
+ } else if(streq(method, "getsecret")) {
+ if (argc < 3)
+ show_usage(argv[0]);
+ print_secret(argv[2], argv[3], false);
} else if(streq(method, "getcodexsecret")) {
if (argc < 4)
show_usage(argv[0]);
- print_codexsecret(argv[2], argv[3]);
+ print_secret(argv[2], argv[3], true);
} else if(streq(method, "getemergencyrecover")) {
if (argc < 3)
show_usage(argv[0]);
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.