From 8d9a750e666dfff7ad962d7a57bcbc97d83d565a Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 10 Apr 2025 13:07:01 +0100 Subject: [PATCH 1/4] HSMD: add new wire BIP137 sign message API Changelog-Added: HSMD: add new wire API to sign messages with bitcoin wallet keys according to BIP137. Signed-off-by: Lagrang3 --- common/hsm_version.h | 3 ++- hsmd/hsmd.c | 2 ++ hsmd/hsmd_wire.csv | 9 ++++++++ hsmd/libhsmd.c | 51 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/common/hsm_version.h b/common/hsm_version.h index b03fb6496c7c..920dc77ee34e 100644 --- a/common/hsm_version.h +++ b/common/hsm_version.h @@ -28,7 +28,8 @@ * v6 no secret from get_per_commitment_point: 0cad1790beb3473d64355f4cb4f64daa80c28c8a241998b7ef0223385d7ffff9 * v6 with sign_bolt12_2 (tweak using node id): 8fcb731279a10af3f95aeb8be1da6b2ced76a1984afa18c5f46a03515d70ea0e * v6 with dev_warn_on_overgrind: a273b68e19336073e551c01a78bcd1e1f8cc510da7d0dde3afc45e249f9830cc -*/ + * v6 with bip137_sign_message: 4bfe28b02e92aae276b8eca2228e32f32d5dee8d5381639e7364939fa2fa1370 + */ #define HSM_MIN_VERSION 5 #define HSM_MAX_VERSION 6 #endif /* LIGHTNING_COMMON_HSM_VERSION_H */ diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index 6a5cc487b90d..ff6d1e7b2c91 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -692,6 +692,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_GET_CHANNEL_BASEPOINTS: case WIRE_HSMD_SIGN_INVOICE: case WIRE_HSMD_SIGN_MESSAGE: + case WIRE_HSMD_BIP137_SIGN_MESSAGE: case WIRE_HSMD_SIGN_OPTION_WILL_FUND_OFFER: case WIRE_HSMD_SIGN_BOLT12: case WIRE_HSMD_SIGN_BOLT12_2: @@ -748,6 +749,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_GET_CHANNEL_BASEPOINTS_REPLY: case WIRE_HSMD_DEV_MEMLEAK_REPLY: case WIRE_HSMD_SIGN_MESSAGE_REPLY: + case WIRE_HSMD_BIP137_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_SIGN_BOLT12_2_REPLY: diff --git a/hsmd/hsmd_wire.csv b/hsmd/hsmd_wire.csv index b758eea28e3a..4a8c7c7d9ec1 100644 --- a/hsmd/hsmd_wire.csv +++ b/hsmd/hsmd_wire.csv @@ -364,6 +364,15 @@ msgdata,hsmd_sign_message,msg,u8,len msgtype,hsmd_sign_message_reply,123 msgdata,hsmd_sign_message_reply,sig,secp256k1_ecdsa_recoverable_signature, +# sign a raw message with a derived key +msgtype,hsmd_bip137_sign_message,45 +msgdata,hsmd_bip137_sign_message,len,u16, +msgdata,hsmd_bip137_sign_message,msg,u8,len +msgdata,hsmd_bip137_sign_message,keyidx,u32, + +msgtype,hsmd_bip137_sign_message_reply,145 +msgdata,hsmd_bip137_sign_message_reply,sig,secp256k1_ecdsa_recoverable_signature, + # lightningd needs to get a scriptPubkey for a utxo with closeinfo msgtype,hsmd_get_output_scriptpubkey,24 msgdata,hsmd_get_output_scriptpubkey,channel_id,u64, diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c index eeaf9a6cff83..1bf58669c5c2 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -136,6 +136,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_GET_CHANNEL_BASEPOINTS: case WIRE_HSMD_DEV_MEMLEAK: case WIRE_HSMD_SIGN_MESSAGE: + case WIRE_HSMD_BIP137_SIGN_MESSAGE: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY: case WIRE_HSMD_SIGN_BOLT12: case WIRE_HSMD_SIGN_BOLT12_2: @@ -182,6 +183,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_GET_CHANNEL_BASEPOINTS_REPLY: case WIRE_HSMD_DEV_MEMLEAK_REPLY: case WIRE_HSMD_SIGN_MESSAGE_REPLY: + case WIRE_HSMD_BIP137_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_SIGN_BOLT12_2_REPLY: @@ -715,6 +717,51 @@ static u8 *handle_sign_message(struct hsmd_client *c, const u8 *msg_in) return towire_hsmd_sign_message_reply(NULL, &rsig); } +/* FIXME: implement BIP0322 signature scheme so that we can support any type of + * address. */ +/* Sign a message with a private key (see BIP137): + * signature = base64(SigRec(SHA256(SHA256( + * "\x18Bitcoin Signed Message:\n" + var_int(len(message)) + message + * )))) */ +static u8 *handle_bip137_sign_message(struct hsmd_client *c, const u8 *msg_in) +{ + u8 *msg; + u32 keyidx; + struct sha256_ctx sctx = SHA256_INIT; + struct sha256_double shad; + secp256k1_ecdsa_recoverable_signature rsig; + struct privkey privkey; + struct pubkey pubkey; + + if (!fromwire_hsmd_bip137_sign_message(tmpctx, msg_in, &msg, &keyidx)) + return hsmd_status_malformed_request(c, msg_in); + + /* double sha256 the message */ + const char header[] = "\x18" + "Bitcoin Signed Message:\n"; + sha256_update(&sctx, (const u8 *)header, strlen(header)); + + u8 vt[VARINT_MAX_LEN]; + size_t msg_len = tal_count(msg); + size_t vtlen = varint_put(vt, msg_len); + sha256_update(&sctx, vt, vtlen); + + sha256_update(&sctx, msg, msg_len); + sha256_double_done(&sctx, &shad); + + /* get the private key BIP32 */ + bitcoin_key(&privkey, &pubkey, keyidx); + + if (!secp256k1_ecdsa_sign_recoverable( + secp256k1_ctx, &rsig, shad.sha.u.u8, privkey.secret.data, NULL, + NULL)) { + return hsmd_status_bad_request(c, msg_in, + "Failed to sign message"); + } + + return towire_hsmd_bip137_sign_message_reply(NULL, &rsig); +} + /*~ lightningd asks us to sign a liquidity ad offer */ static u8 *handle_sign_option_will_fund_offer(struct hsmd_client *c, const u8 *msg_in) @@ -2181,6 +2228,8 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, return handle_preapprove_keysend(client, msg); case WIRE_HSMD_SIGN_MESSAGE: return handle_sign_message(client, msg); + case WIRE_HSMD_BIP137_SIGN_MESSAGE: + return handle_bip137_sign_message(client, msg); case WIRE_HSMD_GET_CHANNEL_BASEPOINTS: return handle_get_channel_basepoints(client, msg); case WIRE_HSMD_CANNOUNCEMENT_SIG_REQ: @@ -2263,6 +2312,7 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, case WIRE_HSMD_GET_CHANNEL_BASEPOINTS_REPLY: case WIRE_HSMD_DEV_MEMLEAK_REPLY: case WIRE_HSMD_SIGN_MESSAGE_REPLY: + case WIRE_HSMD_BIP137_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_SIGN_BOLT12_2_REPLY: @@ -2297,6 +2347,7 @@ u8 *hsmd_init(struct secret hsm_secret, const u64 hsmd_version, WIRE_HSMD_FORGET_CHANNEL, WIRE_HSMD_REVOKE_COMMITMENT_TX, WIRE_HSMD_SIGN_BOLT12_2, + WIRE_HSMD_BIP137_SIGN_MESSAGE, WIRE_HSMD_PREAPPROVE_INVOICE_CHECK, WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK, }; From 9c660c8b072aa5307b992269b4f5235b130b4fe8 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Tue, 15 Apr 2025 10:39:28 +0100 Subject: [PATCH 2/4] add devtool/bip137-verifysignature utility To validate BIP137 signatures produced by core-lightning in tests. Changelog-None. Signed-off-by: Lagrang3 --- devtools/.gitignore | 1 + devtools/Makefile | 4 +- devtools/bip137-verifysignature.c | 114 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 devtools/bip137-verifysignature.c diff --git a/devtools/.gitignore b/devtools/.gitignore index 9787bdc74e61..0266f1158da3 100644 --- a/devtools/.gitignore +++ b/devtools/.gitignore @@ -20,3 +20,4 @@ topology fp16 rune gossmap-compress +bip137-verifysignature diff --git a/devtools/Makefile b/devtools/Makefile index d7f86f9d78a4..bdceecc7e459 100644 --- a/devtools/Makefile +++ b/devtools/Makefile @@ -1,4 +1,4 @@ -DEVTOOLS := devtools/bolt11-cli devtools/decodemsg devtools/onion devtools/dump-gossipstore devtools/gossipwith devtools/create-gossipstore devtools/mkcommit devtools/mkfunding devtools/mkclose devtools/mkgossip devtools/mkencoded devtools/mkquery devtools/lightning-checkmessage devtools/topology devtools/route devtools/bolt12-cli devtools/encodeaddr devtools/features devtools/fp16 devtools/rune devtools/gossmap-compress +DEVTOOLS := devtools/bolt11-cli devtools/decodemsg devtools/onion devtools/dump-gossipstore devtools/gossipwith devtools/create-gossipstore devtools/mkcommit devtools/mkfunding devtools/mkclose devtools/mkgossip devtools/mkencoded devtools/mkquery devtools/lightning-checkmessage devtools/topology devtools/route devtools/bolt12-cli devtools/encodeaddr devtools/features devtools/fp16 devtools/rune devtools/gossmap-compress devtools/bip137-verifysignature ifeq ($(HAVE_SQLITE3),1) DEVTOOLS += devtools/checkchannels endif @@ -99,6 +99,8 @@ devtools/mkquery: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/t devtools/lightning-checkmessage: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/lightning-checkmessage.o +devtools/bip137-verifysignature: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/bip137-verifysignature.o + devtools/route: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o wire/tlvstream.o common/gossmap.o common/fp16.o common/random_select.o common/route.o common/dijkstra.o devtools/clean_topo.o devtools/route.o devtools/topology: $(DEVTOOLS_COMMON_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o wire/tlvstream.o common/gossmap.o common/fp16.o common/random_select.o common/dijkstra.o common/route.o devtools/clean_topo.o devtools/topology.o diff --git a/devtools/bip137-verifysignature.c b/devtools/bip137-verifysignature.c new file mode 100644 index 000000000000..48db7b95db55 --- /dev/null +++ b/devtools/bip137-verifysignature.c @@ -0,0 +1,114 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void usage(void) +{ + fprintf(stderr, + "Usage: bip137-verifysignature message hex-sig [address] [network]\n" + "If key does not match, signature is not valid!\n"); + exit(1); +} + +static char *encode_pubkey_to_p2wpkh_addr(const tal_t *ctx, + const struct pubkey *pubkey, + const struct chainparams *chain) +{ + char *out; + const char *hrp; + struct ripemd160 h160; + bool ok; + hrp = chain->onchain_hrp; + + /* out buffer is 73 + strlen(human readable part), + * see common/bech32.h*/ + out = tal_arr(ctx, char, 73 + strlen(hrp)); + pubkey_to_hash160(pubkey, &h160); + ok = segwit_addr_encode(out, hrp, 0, h160.u.u8, sizeof(h160)); + if(!ok) + tal_free(out); + return out; +} + +int main(int argc, char *argv[]) +{ + u8 *sig; + u8 varint[VARINT_MAX_LEN]; + size_t varintlen, msg_len; + secp256k1_ecdsa_recoverable_signature rsig; + struct sha256_ctx sctx = SHA256_INIT; + struct sha256_double shad; + struct pubkey reckey; + const char *addr; + const struct chainparams *chain = NULL; + const char *input_chain = NULL, *input_address = NULL; + + setup_locale(); + err_set_progname(argv[0]); + wally_init(0); + secp256k1_ctx = wally_get_secp_context(); + + if (argc != 3 && argc != 4 && argc != 5) + usage(); + if (argc > 3) + input_address = argv[3]; + if (argc > 4) + input_chain = argv[4]; + + sig = tal_hexdata(NULL, argv[2], strlen(argv[2])); + if (!sig) + errx(1, "Not a valid hex string"); + + if (sig[0] < 39 || sig[0] >= 43) + errx(1, + "Signature header does not correspond to a P2WPKH type"); + + if (!secp256k1_ecdsa_recoverable_signature_parse_compact( + secp256k1_ctx, &rsig, sig + 1, sig[0] - 39)) + errx(1, "Signature not parsable"); + + sha256_update(&sctx, + "\x18" + "Bitcoin Signed Message:\n", + strlen("\x18" + "Bitcoin Signed Message:\n")); + msg_len = strlen(argv[1]); + varintlen = varint_put(varint, msg_len); + sha256_update(&sctx, varint, varintlen); + sha256_update(&sctx, argv[1], msg_len); + sha256_double_done(&sctx, &shad); + + if (!secp256k1_ecdsa_recover(secp256k1_ctx, &reckey.pubkey, &rsig, + shad.sha.u.u8)) + errx(1, "Signature not recoverable"); + + if (input_chain) { + chain = chainparams_for_network(input_chain); + if (!chain) + errx(1, "Invalid network"); + } else { + /* By default, assume we are verifying a mainnet signature. */ + chain = chainparams_for_network("bitcoin"); + } + addr = encode_pubkey_to_p2wpkh_addr(NULL, &reckey, chain); + if (!addr) + errx(1, "Failed to derive address from recovered key"); + if (input_address) { + if (!streq(addr, input_address)) + errx(1, "Signature is invalid"); + printf("Signature is valid!\n"); + } else + printf("Signature claims to be from address %s\n", addr); + return 0; +} + From 486bb69fe15fef30ab6dee147168077f429eac35 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 10 Apr 2025 13:19:23 +0100 Subject: [PATCH 3/4] add signmessagewithkey RPC signmessagewithkey: allows to sign a message with a key associated with one bitcoin address in our wallet. Changelog-Added: add a new rpc command signmessagewithkey to sign input messages with keys from our wallet. Signed-off-by: Lagrang3 --- contrib/msggen/msggen/schema.json | 93 ++++++++++++++++++++++ doc/schemas/signmessagewithkey.json | 93 ++++++++++++++++++++++ tests/autogenerate-rpc-examples.py | 2 + tests/test_misc.py | 37 +++++++++ wallet/walletrpc.c | 115 ++++++++++++++++++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 doc/schemas/signmessagewithkey.json diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 22ae40142b30..2cd95ee52b34 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -32878,6 +32878,99 @@ } ] }, + "signmessagewithkey.json": { + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "signmessagewithkey", + "title": "Command to create a signature using a key from the wallet", + "description": [ + "The **signmessagewithkey** RPC command creates a digital signature of *message* using the key associated with the address provided in the input.", + "The signature scheme follows the BIP137 specification." + ], + "added": "v25.05", + "request": { + "required": [ + "message", + "address" + ], + "additionalProperties": false, + "properties": { + "message": { + "type": "string", + "description": [ + "Less than 65536 characters long message to be signed by the node." + ] + }, + "address": { + "type": "string", + "description": [ + "A Bitcoin accepted type address for lookup in the list of addresses issued to date.", + "Only P2WPKH type addresses are supported" + ] + } + } + }, + "response": { + "required": [ + "address", + "pubkey", + "signature", + "base64" + ], + "additionalProperties": false, + "properties": { + "address": { + "type": "string", + "description": [ + "The bitcoin address used for signing." + ] + }, + "pubkey": { + "type": "pubkey", + "description": [ + "The public key associated with the bitcoin address provided." + ] + }, + "signature": { + "type": "hex", + "description": [ + "The signature." + ] + }, + "base64": { + "type": "string", + "description": [ + "The signature encoded in base64." + ] + } + } + }, + "author": [ + "Lagrang3 <> is mainly responsible." + ], + "see_also": [], + "resources": [ + "Main web site: " + ], + "examples": [ + { + "request": { + "id": "example:signmessagewithkey#1", + "method": "signmessagewithkey", + "params": { + "message": "a test message", + "address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un" + } + }, + "response": { + "address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un", + "pubkey": "03bc4a456585ba21ba26af4a0e5399ec76410b2e0ca67db0f3bcb2f47b232fa4b0", + "signature": "28564edf260a72d991cbb38cf608e293124f8b8f478d13d4544fe27b9d76c65df1284ca395ccdfd3d5f151729ef18f56c028f5f860155d6aa4d0aaaa176a00db01", + "base64": "KFZO3yYKctmRy7OM9gjikxJPi49HjRPUVE/ie512xl3xKEyjlczf09XxUXKe8Y9WwCj1+GAVXWqk0KqqF2oA2wE=" + } + } + ] + }, "signpsbt.json": { "$schema": "../rpc-schema-draft.json", "type": "object", diff --git a/doc/schemas/signmessagewithkey.json b/doc/schemas/signmessagewithkey.json new file mode 100644 index 000000000000..f24ed027a54b --- /dev/null +++ b/doc/schemas/signmessagewithkey.json @@ -0,0 +1,93 @@ +{ + "$schema": "../rpc-schema-draft.json", + "type": "object", + "rpc": "signmessagewithkey", + "title": "Command to create a signature using a key from the wallet", + "description": [ + "The **signmessagewithkey** RPC command creates a digital signature of *message* using the key associated with the address provided in the input.", + "The signature scheme follows the BIP137 specification." + ], + "added": "v25.05", + "request": { + "required": [ + "message", + "address" + ], + "additionalProperties": false, + "properties": { + "message": { + "type": "string", + "description": [ + "Less than 65536 characters long message to be signed by the node." + ] + }, + "address": { + "type": "string", + "description": [ + "A Bitcoin accepted type address for lookup in the list of addresses issued to date.", + "Only P2WPKH type addresses are supported" + ] + } + } + }, + "response": { + "required": [ + "address", + "pubkey", + "signature", + "base64" + ], + "additionalProperties": false, + "properties": { + "address": { + "type": "string", + "description": [ + "The bitcoin address used for signing." + ] + }, + "pubkey": { + "type": "pubkey", + "description": [ + "The public key associated with the bitcoin address provided." + ] + }, + "signature": { + "type": "hex", + "description": [ + "The signature." + ] + }, + "base64": { + "type": "string", + "description": [ + "The signature encoded in base64." + ] + } + } + }, + "author": [ + "Lagrang3 <> is mainly responsible." + ], + "see_also": [], + "resources": [ + "Main web site: " + ], + "examples": [ + { + "request": { + "id": "example:signmessagewithkey#1", + "method": "signmessagewithkey", + "params": { + "message": "a test message", + "address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un" + } + }, + "response": { + "address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un", + "pubkey": "03bc4a456585ba21ba26af4a0e5399ec76410b2e0ca67db0f3bcb2f47b232fa4b0", + "signature": "28564edf260a72d991cbb38cf608e293124f8b8f478d13d4544fe27b9d76c65df1284ca395ccdfd3d5f151729ef18f56c028f5f860155d6aa4d0aaaa176a00db01", + "base64": "KFZO3yYKctmRy7OM9gjikxJPi49HjRPUVE/ie512xl3xKEyjlczf09XxUXKe8Y9WwCj1+GAVXWqk0KqqF2oA2wE=" + } + } + ] +} diff --git a/tests/autogenerate-rpc-examples.py b/tests/autogenerate-rpc-examples.py index b7221de41b05..52a0555f6e81 100644 --- a/tests/autogenerate-rpc-examples.py +++ b/tests/autogenerate-rpc-examples.py @@ -1211,6 +1211,8 @@ def generate_utils_examples(l1, l2, l3, l4, l5, l6, c23_2, c34_2, inv_l11, inv_l update_example(node=l2, method='signmessage', params={'message': 'message for you'}) update_example(node=l2, method='checkmessage', params={'message': 'testcase to check new rpc error', 'zbase': 'd66bqz3qsku5fxtqsi37j11pci47ydxa95iusphutggz9ezaxt56neh77kxe5hyr41kwgkncgiu94p9ecxiexgpgsz8daoq4tw8kj8yx', 'pubkey': '03be3b0e9992153b1d5a6e1623670b6c3663f72ce6cf2e0dd39c0a373a7de5a3b7'}) update_example(node=l2, method='checkmessage', params={'message': 'this is a test!', 'zbase': 'd6tqaeuonjhi98mmont9m4wag7gg4krg1f4txonug3h31e9h6p6k6nbwjondnj46dkyausobstnk7fhyy998bhgc1yr98dfmhb4k54d7'}) + addr = l2.rpc.newaddr('bech32')['bech32'] + update_example(node=l2, method='signmessagewithkey', params={'message': 'a test message', 'address': addr}) decodepay_res1 = update_example(node=l2, method='decodepay', params={'bolt11': inv_l11['bolt11']}) update_example(node=l2, method='decode', params=[rune_l21['rune']]) decode_res2 = update_example(node=l2, method='decode', params=[inv_l22['bolt11']]) diff --git a/tests/test_misc.py b/tests/test_misc.py index fc01ca883d34..4812e11d19f6 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,3 +1,4 @@ +import base64 from bitcoin.rpc import RawProxy from decimal import Decimal from fixtures import * # noqa: F401,F403 @@ -2484,6 +2485,42 @@ def test_signmessage(node_factory): l2.rpc.checkmessage(message="wrong zbase format", zbase="wrong zbase format") +def test_signmessagewithkey(node_factory, chainparams): + l1, l2 = node_factory.get_nodes(2) + message = "a test message" + addr_bech32 = l1.rpc.newaddr("bech32")["bech32"] + addr_other = l2.rpc.newaddr("bech32")["bech32"] + if TEST_NETWORK != "liquid-regtest": + # refuse to sign if the address is not a P2WPKH + addr_p2tr = l1.rpc.newaddr("p2tr")["p2tr"] + with pytest.raises( + RpcError, match=r"Address is not p2wpkh and it is not supported" + ): + l1.rpc.signmessagewithkey(message, addr_p2tr) + + # refuse to sign if the address does not belong to us + with pytest.raises( + RpcError, match=r"Address is not found in the wallet\'s database" + ): + l1.rpc.signmessagewithkey(message, addr_other) + response = l1.rpc.signmessagewithkey(message, addr_bech32) + assert response["address"] == addr_bech32 + signature = base64.b64decode(response["base64"]) + assert signature.hex() == response["signature"] + assert ( + subprocess.check_output( + [ + "devtools/bip137-verifysignature", + message, + response["signature"], + response["address"], + chainparams["name"], + ] + ).decode("utf-8") + == "Signature is valid!\n" + ) + + def test_include(node_factory): l1 = node_factory.get_node(start=False) diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 20c77c857915..29beb0aaaf6f 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -1102,3 +1103,117 @@ static const struct json_command sendpsbt_command = { }; AUTODATA(json_command, &sendpsbt_command); + +static struct command_result * +json_signmessagewithkey(struct command *cmd, const char *buffer, + const jsmntok_t *obj UNNEEDED, const jsmntok_t *params) +{ + /* decoding the address */ + const u8 *scriptpubkey; + const char *message; + + /* from wallet BIP32 */ + struct pubkey pubkey; + + if (!param( + cmd, buffer, params, + p_req("message", param_string, &message), + p_req("address", param_bitcoin_address, &scriptpubkey), + NULL)) + return command_param_failed(); + + const size_t script_len = tal_bytelen(scriptpubkey); + + /* FIXME: we already had the address from the input */ + char *addr; + addr = encode_scriptpubkey_to_addr(tmpctx, chainparams, scriptpubkey); + + if (!is_p2wpkh(scriptpubkey, script_len, NULL)) { + /* FIXME add support for BIP 322 */ + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Address is not p2wpkh and " + "it is not supported for signing"); + } + + if (!hsm_capable(cmd->ld, WIRE_HSMD_BIP137_SIGN_MESSAGE)) { + return command_fail( + cmd, JSONRPC2_INVALID_PARAMS, + "HSM does not support signing BIP137 signing."); + } + + const u32 bip32_max_index = + db_get_intvar(cmd->ld->wallet->db, "bip32_max_index", 0); + bool match_found = false; + u32 keyidx; + enum addrtype addrtype; + + /* loop over all generated keys, find a matching key */ + for (keyidx = 1; keyidx <= bip32_max_index; keyidx++) { + bip32_pubkey(cmd->ld, &pubkey, keyidx); + u8 *redeemscript_p2wpkh; + char *out_p2wpkh = encode_pubkey_to_addr( + cmd, &pubkey, ADDR_BECH32, &redeemscript_p2wpkh); + if (!out_p2wpkh) { + abort(); + } + /* wallet_get_addrtype fails for entries prior to v24.11, all + * address types are assumed in that case. */ + if (!wallet_get_addrtype(cmd->ld->wallet, keyidx, &addrtype)) + addrtype = ADDR_ALL; + if (streq(addr, out_p2wpkh) && + (addrtype == ADDR_BECH32 || addrtype == ADDR_ALL)) { + match_found = true; + break; + } + } + + if (!match_found) { + return command_fail( + cmd, JSONRPC2_INVALID_PARAMS, + "Address is not found in the wallet's database"); + } + + /* wire to hsmd a sign request */ + u8 *msg = towire_hsmd_bip137_sign_message( + cmd, tal_dup_arr(tmpctx, u8, (u8 *)message, strlen(message), 0), + keyidx); + if (!wire_sync_write(cmd->ld->hsm_fd, take(msg))) { + fatal("Could not write sign_with_key to HSM: %s", + strerror(errno)); + } + + /* read form hsmd a sign reply */ + msg = wire_sync_read(cmd, cmd->ld->hsm_fd); + + int recid; + u8 sig[65]; + secp256k1_ecdsa_recoverable_signature rsig; + + if (!fromwire_hsmd_bip137_sign_message_reply(msg, &rsig)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "HSM gave bad sign_with_key_reply %s", + tal_hex(tmpctx, msg)); + } + + secp256k1_ecdsa_recoverable_signature_serialize_compact( + secp256k1_ctx, sig + 1, &recid, &rsig); + /* this is the header value for P2WPKH specified in BIP137 */ + sig[0] = recid + 39; + + /* FIXME: Given the fact that we plan to extend support for BIP322 + * signature in the future making a pubkey output here makes less sense. */ + struct json_stream *response; + response = json_stream_success(cmd); + json_add_string(response, "address", addr); + json_add_pubkey(response, "pubkey", &pubkey); + json_add_hex(response, "signature", sig, sizeof(sig)); + json_add_string(response, "base64", + b64_encode(tmpctx, sig, sizeof(sig))); + return command_success(cmd, response); +} + +static const struct json_command signmessagewithkey_command = { + "signmessagewithkey", + json_signmessagewithkey +}; +AUTODATA(json_command, &signmessagewithkey_command); From 39f66541cf388ce297a388913df228069e223518 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Mon, 28 Apr 2025 11:50:57 +0100 Subject: [PATCH 4/4] hsmd: refactor hsmd_init Add preapprove_check capabilities: WIRE_HSMD_PREAPPROVE_INCOICE_CHECK and WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK to the capabilities array if dev_no_preapprove_check is not set. Do not assume those occupy the last two slots in the array. Changelog-None Signed-off-by: Lagrang3 --- hsmd/libhsmd.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c index 1bf58669c5c2..7ce12bc083cf 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -2348,10 +2348,8 @@ u8 *hsmd_init(struct secret hsm_secret, const u64 hsmd_version, WIRE_HSMD_REVOKE_COMMITMENT_TX, WIRE_HSMD_SIGN_BOLT12_2, WIRE_HSMD_BIP137_SIGN_MESSAGE, - WIRE_HSMD_PREAPPROVE_INVOICE_CHECK, - WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK, }; - const u32 *caps; + u32 *caps; /*~ Don't swap this. */ sodium_mlock(secretstuff.hsm_secret.data, @@ -2474,14 +2472,11 @@ u8 *hsmd_init(struct secret hsm_secret, const u64 hsmd_version, "derived secrets", strlen("derived secrets")); /* Capabilities arg needs to be a tal array */ - if (dev_no_preapprove_check) { - /* Skip preapprove capabilities */ - caps = tal_dup_arr(tmpctx, u32, - capabilities, ARRAY_SIZE(capabilities) - 2, - 0); - } else { - caps = tal_dup_arr(tmpctx, u32, - capabilities, ARRAY_SIZE(capabilities), 0); + caps = + tal_dup_arr(tmpctx, u32, capabilities, ARRAY_SIZE(capabilities), 0); + if (!dev_no_preapprove_check) { + tal_arr_expand(&caps, WIRE_HSMD_PREAPPROVE_INVOICE_CHECK); + tal_arr_expand(&caps, WIRE_HSMD_PREAPPROVE_KEYSEND_CHECK); } /*~ Note: marshalling a bip32 tree only marshals the public side,