Skip to content

Commit 486bb69

Browse files
Lagrang3rustyrussell
authored andcommitted
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 <lagrang3@protonmail.com>
1 parent 9c660c8 commit 486bb69

File tree

5 files changed

+340
-0
lines changed

5 files changed

+340
-0
lines changed

contrib/msggen/msggen/schema.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32878,6 +32878,99 @@
3287832878
}
3287932879
]
3288032880
},
32881+
"signmessagewithkey.json": {
32882+
"$schema": "../rpc-schema-draft.json",
32883+
"type": "object",
32884+
"rpc": "signmessagewithkey",
32885+
"title": "Command to create a signature using a key from the wallet",
32886+
"description": [
32887+
"The **signmessagewithkey** RPC command creates a digital signature of *message* using the key associated with the address provided in the input.",
32888+
"The signature scheme follows the BIP137 specification."
32889+
],
32890+
"added": "v25.05",
32891+
"request": {
32892+
"required": [
32893+
"message",
32894+
"address"
32895+
],
32896+
"additionalProperties": false,
32897+
"properties": {
32898+
"message": {
32899+
"type": "string",
32900+
"description": [
32901+
"Less than 65536 characters long message to be signed by the node."
32902+
]
32903+
},
32904+
"address": {
32905+
"type": "string",
32906+
"description": [
32907+
"A Bitcoin accepted type address for lookup in the list of addresses issued to date.",
32908+
"Only P2WPKH type addresses are supported"
32909+
]
32910+
}
32911+
}
32912+
},
32913+
"response": {
32914+
"required": [
32915+
"address",
32916+
"pubkey",
32917+
"signature",
32918+
"base64"
32919+
],
32920+
"additionalProperties": false,
32921+
"properties": {
32922+
"address": {
32923+
"type": "string",
32924+
"description": [
32925+
"The bitcoin address used for signing."
32926+
]
32927+
},
32928+
"pubkey": {
32929+
"type": "pubkey",
32930+
"description": [
32931+
"The public key associated with the bitcoin address provided."
32932+
]
32933+
},
32934+
"signature": {
32935+
"type": "hex",
32936+
"description": [
32937+
"The signature."
32938+
]
32939+
},
32940+
"base64": {
32941+
"type": "string",
32942+
"description": [
32943+
"The signature encoded in base64."
32944+
]
32945+
}
32946+
}
32947+
},
32948+
"author": [
32949+
"Lagrang3 <<lagrang3@protonmail.com>> is mainly responsible."
32950+
],
32951+
"see_also": [],
32952+
"resources": [
32953+
"Main web site: <https://github.com/ElementsProject/lightning>"
32954+
],
32955+
"examples": [
32956+
{
32957+
"request": {
32958+
"id": "example:signmessagewithkey#1",
32959+
"method": "signmessagewithkey",
32960+
"params": {
32961+
"message": "a test message",
32962+
"address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un"
32963+
}
32964+
},
32965+
"response": {
32966+
"address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un",
32967+
"pubkey": "03bc4a456585ba21ba26af4a0e5399ec76410b2e0ca67db0f3bcb2f47b232fa4b0",
32968+
"signature": "28564edf260a72d991cbb38cf608e293124f8b8f478d13d4544fe27b9d76c65df1284ca395ccdfd3d5f151729ef18f56c028f5f860155d6aa4d0aaaa176a00db01",
32969+
"base64": "KFZO3yYKctmRy7OM9gjikxJPi49HjRPUVE/ie512xl3xKEyjlczf09XxUXKe8Y9WwCj1+GAVXWqk0KqqF2oA2wE="
32970+
}
32971+
}
32972+
]
32973+
},
3288132974
"signpsbt.json": {
3288232975
"$schema": "../rpc-schema-draft.json",
3288332976
"type": "object",

doc/schemas/signmessagewithkey.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"$schema": "../rpc-schema-draft.json",
3+
"type": "object",
4+
"rpc": "signmessagewithkey",
5+
"title": "Command to create a signature using a key from the wallet",
6+
"description": [
7+
"The **signmessagewithkey** RPC command creates a digital signature of *message* using the key associated with the address provided in the input.",
8+
"The signature scheme follows the BIP137 specification."
9+
],
10+
"added": "v25.05",
11+
"request": {
12+
"required": [
13+
"message",
14+
"address"
15+
],
16+
"additionalProperties": false,
17+
"properties": {
18+
"message": {
19+
"type": "string",
20+
"description": [
21+
"Less than 65536 characters long message to be signed by the node."
22+
]
23+
},
24+
"address": {
25+
"type": "string",
26+
"description": [
27+
"A Bitcoin accepted type address for lookup in the list of addresses issued to date.",
28+
"Only P2WPKH type addresses are supported"
29+
]
30+
}
31+
}
32+
},
33+
"response": {
34+
"required": [
35+
"address",
36+
"pubkey",
37+
"signature",
38+
"base64"
39+
],
40+
"additionalProperties": false,
41+
"properties": {
42+
"address": {
43+
"type": "string",
44+
"description": [
45+
"The bitcoin address used for signing."
46+
]
47+
},
48+
"pubkey": {
49+
"type": "pubkey",
50+
"description": [
51+
"The public key associated with the bitcoin address provided."
52+
]
53+
},
54+
"signature": {
55+
"type": "hex",
56+
"description": [
57+
"The signature."
58+
]
59+
},
60+
"base64": {
61+
"type": "string",
62+
"description": [
63+
"The signature encoded in base64."
64+
]
65+
}
66+
}
67+
},
68+
"author": [
69+
"Lagrang3 <<lagrang3@protonmail.com>> is mainly responsible."
70+
],
71+
"see_also": [],
72+
"resources": [
73+
"Main web site: <https://github.com/ElementsProject/lightning>"
74+
],
75+
"examples": [
76+
{
77+
"request": {
78+
"id": "example:signmessagewithkey#1",
79+
"method": "signmessagewithkey",
80+
"params": {
81+
"message": "a test message",
82+
"address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un"
83+
}
84+
},
85+
"response": {
86+
"address": "bcrt1qgrh5vtf63mtayzhxwp480aww3j3qfr5qpq65un",
87+
"pubkey": "03bc4a456585ba21ba26af4a0e5399ec76410b2e0ca67db0f3bcb2f47b232fa4b0",
88+
"signature": "28564edf260a72d991cbb38cf608e293124f8b8f478d13d4544fe27b9d76c65df1284ca395ccdfd3d5f151729ef18f56c028f5f860155d6aa4d0aaaa176a00db01",
89+
"base64": "KFZO3yYKctmRy7OM9gjikxJPi49HjRPUVE/ie512xl3xKEyjlczf09XxUXKe8Y9WwCj1+GAVXWqk0KqqF2oA2wE="
90+
}
91+
}
92+
]
93+
}

tests/autogenerate-rpc-examples.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,8 @@ def generate_utils_examples(l1, l2, l3, l4, l5, l6, c23_2, c34_2, inv_l11, inv_l
12111211
update_example(node=l2, method='signmessage', params={'message': 'message for you'})
12121212
update_example(node=l2, method='checkmessage', params={'message': 'testcase to check new rpc error', 'zbase': 'd66bqz3qsku5fxtqsi37j11pci47ydxa95iusphutggz9ezaxt56neh77kxe5hyr41kwgkncgiu94p9ecxiexgpgsz8daoq4tw8kj8yx', 'pubkey': '03be3b0e9992153b1d5a6e1623670b6c3663f72ce6cf2e0dd39c0a373a7de5a3b7'})
12131213
update_example(node=l2, method='checkmessage', params={'message': 'this is a test!', 'zbase': 'd6tqaeuonjhi98mmont9m4wag7gg4krg1f4txonug3h31e9h6p6k6nbwjondnj46dkyausobstnk7fhyy998bhgc1yr98dfmhb4k54d7'})
1214+
addr = l2.rpc.newaddr('bech32')['bech32']
1215+
update_example(node=l2, method='signmessagewithkey', params={'message': 'a test message', 'address': addr})
12141216
decodepay_res1 = update_example(node=l2, method='decodepay', params={'bolt11': inv_l11['bolt11']})
12151217
update_example(node=l2, method='decode', params=[rune_l21['rune']])
12161218
decode_res2 = update_example(node=l2, method='decode', params=[inv_l22['bolt11']])

tests/test_misc.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
from bitcoin.rpc import RawProxy
23
from decimal import Decimal
34
from fixtures import * # noqa: F401,F403
@@ -2484,6 +2485,42 @@ def test_signmessage(node_factory):
24842485
l2.rpc.checkmessage(message="wrong zbase format", zbase="wrong zbase format")
24852486

24862487

2488+
def test_signmessagewithkey(node_factory, chainparams):
2489+
l1, l2 = node_factory.get_nodes(2)
2490+
message = "a test message"
2491+
addr_bech32 = l1.rpc.newaddr("bech32")["bech32"]
2492+
addr_other = l2.rpc.newaddr("bech32")["bech32"]
2493+
if TEST_NETWORK != "liquid-regtest":
2494+
# refuse to sign if the address is not a P2WPKH
2495+
addr_p2tr = l1.rpc.newaddr("p2tr")["p2tr"]
2496+
with pytest.raises(
2497+
RpcError, match=r"Address is not p2wpkh and it is not supported"
2498+
):
2499+
l1.rpc.signmessagewithkey(message, addr_p2tr)
2500+
2501+
# refuse to sign if the address does not belong to us
2502+
with pytest.raises(
2503+
RpcError, match=r"Address is not found in the wallet\'s database"
2504+
):
2505+
l1.rpc.signmessagewithkey(message, addr_other)
2506+
response = l1.rpc.signmessagewithkey(message, addr_bech32)
2507+
assert response["address"] == addr_bech32
2508+
signature = base64.b64decode(response["base64"])
2509+
assert signature.hex() == response["signature"]
2510+
assert (
2511+
subprocess.check_output(
2512+
[
2513+
"devtools/bip137-verifysignature",
2514+
message,
2515+
response["signature"],
2516+
response["address"],
2517+
chainparams["name"],
2518+
]
2519+
).decode("utf-8")
2520+
== "Signature is valid!\n"
2521+
)
2522+
2523+
24872524
def test_include(node_factory):
24882525
l1 = node_factory.get_node(start=False)
24892526

wallet/walletrpc.c

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <ccan/array_size/array_size.h>
55
#include <ccan/cast/cast.h>
66
#include <common/addr.h>
7+
#include <common/base64.h>
78
#include <common/bech32.h>
89
#include <common/configdir.h>
910
#include <common/json_command.h>
@@ -1102,3 +1103,117 @@ static const struct json_command sendpsbt_command = {
11021103
};
11031104

11041105
AUTODATA(json_command, &sendpsbt_command);
1106+
1107+
static struct command_result *
1108+
json_signmessagewithkey(struct command *cmd, const char *buffer,
1109+
const jsmntok_t *obj UNNEEDED, const jsmntok_t *params)
1110+
{
1111+
/* decoding the address */
1112+
const u8 *scriptpubkey;
1113+
const char *message;
1114+
1115+
/* from wallet BIP32 */
1116+
struct pubkey pubkey;
1117+
1118+
if (!param(
1119+
cmd, buffer, params,
1120+
p_req("message", param_string, &message),
1121+
p_req("address", param_bitcoin_address, &scriptpubkey),
1122+
NULL))
1123+
return command_param_failed();
1124+
1125+
const size_t script_len = tal_bytelen(scriptpubkey);
1126+
1127+
/* FIXME: we already had the address from the input */
1128+
char *addr;
1129+
addr = encode_scriptpubkey_to_addr(tmpctx, chainparams, scriptpubkey);
1130+
1131+
if (!is_p2wpkh(scriptpubkey, script_len, NULL)) {
1132+
/* FIXME add support for BIP 322 */
1133+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1134+
"Address is not p2wpkh and "
1135+
"it is not supported for signing");
1136+
}
1137+
1138+
if (!hsm_capable(cmd->ld, WIRE_HSMD_BIP137_SIGN_MESSAGE)) {
1139+
return command_fail(
1140+
cmd, JSONRPC2_INVALID_PARAMS,
1141+
"HSM does not support signing BIP137 signing.");
1142+
}
1143+
1144+
const u32 bip32_max_index =
1145+
db_get_intvar(cmd->ld->wallet->db, "bip32_max_index", 0);
1146+
bool match_found = false;
1147+
u32 keyidx;
1148+
enum addrtype addrtype;
1149+
1150+
/* loop over all generated keys, find a matching key */
1151+
for (keyidx = 1; keyidx <= bip32_max_index; keyidx++) {
1152+
bip32_pubkey(cmd->ld, &pubkey, keyidx);
1153+
u8 *redeemscript_p2wpkh;
1154+
char *out_p2wpkh = encode_pubkey_to_addr(
1155+
cmd, &pubkey, ADDR_BECH32, &redeemscript_p2wpkh);
1156+
if (!out_p2wpkh) {
1157+
abort();
1158+
}
1159+
/* wallet_get_addrtype fails for entries prior to v24.11, all
1160+
* address types are assumed in that case. */
1161+
if (!wallet_get_addrtype(cmd->ld->wallet, keyidx, &addrtype))
1162+
addrtype = ADDR_ALL;
1163+
if (streq(addr, out_p2wpkh) &&
1164+
(addrtype == ADDR_BECH32 || addrtype == ADDR_ALL)) {
1165+
match_found = true;
1166+
break;
1167+
}
1168+
}
1169+
1170+
if (!match_found) {
1171+
return command_fail(
1172+
cmd, JSONRPC2_INVALID_PARAMS,
1173+
"Address is not found in the wallet's database");
1174+
}
1175+
1176+
/* wire to hsmd a sign request */
1177+
u8 *msg = towire_hsmd_bip137_sign_message(
1178+
cmd, tal_dup_arr(tmpctx, u8, (u8 *)message, strlen(message), 0),
1179+
keyidx);
1180+
if (!wire_sync_write(cmd->ld->hsm_fd, take(msg))) {
1181+
fatal("Could not write sign_with_key to HSM: %s",
1182+
strerror(errno));
1183+
}
1184+
1185+
/* read form hsmd a sign reply */
1186+
msg = wire_sync_read(cmd, cmd->ld->hsm_fd);
1187+
1188+
int recid;
1189+
u8 sig[65];
1190+
secp256k1_ecdsa_recoverable_signature rsig;
1191+
1192+
if (!fromwire_hsmd_bip137_sign_message_reply(msg, &rsig)) {
1193+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1194+
"HSM gave bad sign_with_key_reply %s",
1195+
tal_hex(tmpctx, msg));
1196+
}
1197+
1198+
secp256k1_ecdsa_recoverable_signature_serialize_compact(
1199+
secp256k1_ctx, sig + 1, &recid, &rsig);
1200+
/* this is the header value for P2WPKH specified in BIP137 */
1201+
sig[0] = recid + 39;
1202+
1203+
/* FIXME: Given the fact that we plan to extend support for BIP322
1204+
* signature in the future making a pubkey output here makes less sense. */
1205+
struct json_stream *response;
1206+
response = json_stream_success(cmd);
1207+
json_add_string(response, "address", addr);
1208+
json_add_pubkey(response, "pubkey", &pubkey);
1209+
json_add_hex(response, "signature", sig, sizeof(sig));
1210+
json_add_string(response, "base64",
1211+
b64_encode(tmpctx, sig, sizeof(sig)));
1212+
return command_success(cmd, response);
1213+
}
1214+
1215+
static const struct json_command signmessagewithkey_command = {
1216+
"signmessagewithkey",
1217+
json_signmessagewithkey
1218+
};
1219+
AUTODATA(json_command, &signmessagewithkey_command);

0 commit comments

Comments
 (0)