From a4dc714cdcf409afcee24c540730ae3bc8bcf82c Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Mon, 5 Dec 2022 20:11:36 -0800 Subject: [PATCH] hsmd: add hsmd_preapprove_keysend and check_preapprovekeysend pay modifier Changelog-added: hsmd: A new message `hsmd_preapprove_keysend` is added. Changelog-added: JSON-RPC: A new command `preapprovekeysend` is added. --- common/jsonrpc_errors.h | 1 + doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-preapprovekeysend.7.md | 61 +++++++++++++++++++++ doc/schemas/preapproveinvoice.request.json | 3 +- doc/schemas/preapprovekeysend.request.json | 27 +++++++++ doc/schemas/preapprovekeysend.schema.json | 6 ++ hsmd/hsmd.c | 2 + hsmd/hsmd_wire.csv | 10 ++++ hsmd/libhsmd.c | 23 ++++++++ lightningd/invoice.c | 44 +++++++++++++++ lightningd/test/run-invoice-select-inchan.c | 6 ++ plugins/keysend.c | 54 ++++++++++++++++++ 13 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 doc/lightning-preapprovekeysend.7.md create mode 100644 doc/schemas/preapprovekeysend.request.json create mode 100644 doc/schemas/preapprovekeysend.schema.json diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index 7afd3dd03..899081765 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -47,6 +47,7 @@ enum jsonrpc_errcode { PAY_STATUS_UNEXPECTED = 211, PAY_INVOICE_REQUEST_INVALID = 212, PAY_INVOICE_PREAPPROVAL_DECLINED = 213, + PAY_KEYSEND_PREAPPROVAL_DECLINED = 214, /* `fundchannel` or `withdraw` errors */ FUND_MAX_EXCEEDED = 300, diff --git a/doc/Makefile b/doc/Makefile index 53de4115b..75a8faa4d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -76,6 +76,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-parsefeerate.7 \ doc/lightning-plugin.7 \ doc/lightning-preapproveinvoice.7 \ + doc/lightning-preapprovekeysend.7 \ doc/lightning-recoverchannel.7 \ doc/lightning-reserveinputs.7 \ doc/lightning-sendinvoice.7 \ diff --git a/doc/index.rst b/doc/index.rst index 4180153c3..730180624 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -106,6 +106,7 @@ Core Lightning Documentation lightning-ping lightning-plugin lightning-preapproveinvoice + lightning-preapprovekeysend lightning-recoverchannel lightning-reserveinputs lightning-sendcustommsg diff --git a/doc/lightning-preapprovekeysend.7.md b/doc/lightning-preapprovekeysend.7.md new file mode 100644 index 000000000..533041818 --- /dev/null +++ b/doc/lightning-preapprovekeysend.7.md @@ -0,0 +1,61 @@ +lightning-preapprovekeysend -- Ask the HSM to preapprove a keysend payment (low-level) +================================================================== + +SYNOPSIS +-------- + +**preapprovekeysend** *destination* *payment\_hash* *amount\_msat* + +DESCRIPTION +----------- + +The **preapprovekeysend** RPC command submits the *destination*, *payment\_hash*, +and *amount\_msat* parameters to the HSM to check that they are approved as a +keysend payment. + +*destination* is a 33 byte, hex-encoded, node ID of the node that the payment should go to. + +*payment\_hash* is the unique identifier of a payment. + +*amount\_msat* is the amount to send in millisatoshi precision; it can +be a whole number, or a whole number with suffix `msat` or `sat`, or a +three decimal point number with suffix `sat`, or an 1 to 11 decimal +point number suffixed by `btc`. + +Generally the **preapprovekeysend** request does not need to be made +explicitly, it is automatically generated as part of a **keysend** request. + +By default, the HSM will approve all **preapprovekeysend** requests. + +If a remote signer is being used it might decline an **preapprovekeysend** +request because it would exceed velocity controls, is not covered by +allowlist controls, was declined manually, or other reasons. + +If a remote signer declines a **preapprovekeysend** request a subsequent +attempt to pay the keysend anyway will fail; the signer will refuse to sign +the commitment. + +RETURN VALUE +------------ + +[comment]: # (GENERATE-FROM-SCHEMA-START) +On success, an empty object is returned. + +[comment]: # (GENERATE-FROM-SCHEMA-END) + +AUTHOR +------ + +Ken Sedgwick <> is mainly responsible. + +SEE ALSO +-------- + +lightning-keysend(7) + +RESOURCES +--------- + +Main web site: + +[comment]: # ( SHA256STAMP:735dd61146b04745f1e884037ead662a386fec2c41e2de1a8698d6bb03f63540) diff --git a/doc/schemas/preapproveinvoice.request.json b/doc/schemas/preapproveinvoice.request.json index 7e80a3f23..27c5cf991 100644 --- a/doc/schemas/preapproveinvoice.request.json +++ b/doc/schemas/preapproveinvoice.request.json @@ -7,7 +7,8 @@ ], "properties": { "bolt11": { - "type": "string" + "type": "string", + "added": "v23.02" } } } diff --git a/doc/schemas/preapprovekeysend.request.json b/doc/schemas/preapprovekeysend.request.json new file mode 100644 index 000000000..38a6d3594 --- /dev/null +++ b/doc/schemas/preapprovekeysend.request.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "destination", + "payment_hash", + "amount_msat" + ], + "properties": { + "destination": { + "type": "pubkey", + "added": "v23.02" + }, + "payment_hash": { + "type": "hex", + "added": "v23.02", + "description": "the hash of the *payment_preimage* which will prove payment", + "maxLength": 64, + "minLength": 64 + }, + "amount_msat": { + "type": "msat", + "added": "v23.02" + } + } +} diff --git a/doc/schemas/preapprovekeysend.schema.json b/doc/schemas/preapprovekeysend.schema.json new file mode 100644 index 000000000..1aad2dcae --- /dev/null +++ b/doc/schemas/preapprovekeysend.schema.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} +} diff --git a/hsmd/hsmd.c b/hsmd/hsmd.c index 59aa65bad..db0548a6a 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -669,6 +669,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_SIGN_OPTION_WILL_FUND_OFFER: case WIRE_HSMD_SIGN_BOLT12: case WIRE_HSMD_PREAPPROVE_INVOICE: + case WIRE_HSMD_PREAPPROVE_KEYSEND: case WIRE_HSMD_ECDH_REQ: case WIRE_HSMD_CHECK_FUTURE_SECRET: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY: @@ -710,6 +711,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_PREAPPROVE_INVOICE_REPLY: + case WIRE_HSMD_PREAPPROVE_KEYSEND_REPLY: return bad_req_fmt(conn, c, c->msg_in, "Received an incoming message of type %s, " "which is not a request", diff --git a/hsmd/hsmd_wire.csv b/hsmd/hsmd_wire.csv index 0cb4c3e75..b2ee907d5 100644 --- a/hsmd/hsmd_wire.csv +++ b/hsmd/hsmd_wire.csv @@ -121,6 +121,16 @@ msgdata,hsmd_preapprove_invoice,invstring,wirestring, msgtype,hsmd_preapprove_invoice_reply,138 msgdata,hsmd_preapprove_invoice_reply,approved,bool, +# Preapprove a keysend payment +msgtype,hsmd_preapprove_keysend,39 +msgdata,hsmd_preapprove_keysend,destination,node_id, +msgdata,hsmd_preapprove_keysend,payment_hash,sha256, +msgdata,hsmd_preapprove_keysend,amount_msat,amount_msat, + +# Result is true if approved, declined if false +msgtype,hsmd_preapprove_keysend_reply,139 +msgdata,hsmd_preapprove_keysend_reply,approved,bool, + # Give me ECDH(node-id-secret,point) msgtype,hsmd_ecdh_req,1 msgdata,hsmd_ecdh_req,point,pubkey, diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c index 21ab38b5f..ed11560b9 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -120,6 +120,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY: case WIRE_HSMD_SIGN_BOLT12: case WIRE_HSMD_PREAPPROVE_INVOICE: + case WIRE_HSMD_PREAPPROVE_KEYSEND: case WIRE_HSMD_DERIVE_SECRET: return (client->capabilities & HSM_CAP_MASTER) != 0; @@ -151,6 +152,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_PREAPPROVE_INVOICE_REPLY: + case WIRE_HSMD_PREAPPROVE_KEYSEND_REPLY: case WIRE_HSMD_DERIVE_SECRET_REPLY: break; } @@ -677,6 +679,24 @@ static u8 *handle_preapprove_invoice(struct hsmd_client *c, const u8 *msg_in) return towire_hsmd_preapprove_invoice_reply(NULL, approved); } +/*~ lightningd asks us to approve a keysend payment. This stub implementation + * is overriden by fully validating signers that need to track keysend + * payments. */ +static u8 *handle_preapprove_keysend(struct hsmd_client *c, const u8 *msg_in) +{ + struct node_id destination; + struct sha256 payment_hash; + struct amount_msat amount_msat; + bool approved; + if (!fromwire_hsmd_preapprove_keysend(msg_in, &destination, &payment_hash, &amount_msat)) + return hsmd_status_malformed_request(c, msg_in); + + /* This stub always approves */ + approved = true; + + return towire_hsmd_preapprove_keysend_reply(NULL, approved); +} + /*~ Lightning invoices, defined by BOLT 11, are signed. This has been * surprisingly controversial; it means a node needs to be online to create * invoices. However, it seems clear to me that in a world without @@ -1592,6 +1612,8 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, return handle_sign_bolt12(client, msg); case WIRE_HSMD_PREAPPROVE_INVOICE: return handle_preapprove_invoice(client, msg); + case WIRE_HSMD_PREAPPROVE_KEYSEND: + return handle_preapprove_keysend(client, msg); case WIRE_HSMD_SIGN_MESSAGE: return handle_sign_message(client, msg); case WIRE_HSMD_GET_CHANNEL_BASEPOINTS: @@ -1656,6 +1678,7 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: case WIRE_HSMD_PREAPPROVE_INVOICE_REPLY: + case WIRE_HSMD_PREAPPROVE_KEYSEND_REPLY: break; } return hsmd_status_bad_request(client, msg, "Unknown request"); diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 26a43ab1f..1c890931b 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -1853,3 +1853,47 @@ static const struct json_command preapproveinvoice_command = { "Ask the HSM to preapprove an invoice." }; AUTODATA(json_command, &preapproveinvoice_command); + +static struct command_result *json_preapprovekeysend(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct node_id *destination; + struct sha256 *payment_hash; + struct amount_msat *amount; + + struct json_stream *response; + bool approved; + + if (!param(cmd, buffer, params, + p_req("destination", param_node_id, &destination), + p_req("payment_hash", param_sha256, &payment_hash), + p_req("amount_msat|msatoshi", param_msat, &amount), + NULL)) + return command_param_failed(); + + u8 *msg = towire_hsmd_preapprove_keysend(NULL, destination, payment_hash, *amount); + + if (!wire_sync_write(cmd->ld->hsm_fd, take(msg))) + fatal("Could not write to HSM: %s", strerror(errno)); + + msg = wire_sync_read(tmpctx, cmd->ld->hsm_fd); + if (!fromwire_hsmd_preapprove_keysend_reply(msg, &approved)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "HSM gave bad preapprove_keysend_reply %s", tal_hex(msg, msg)); + + if (!approved) + return command_fail(cmd, PAY_KEYSEND_PREAPPROVAL_DECLINED, "keysend was declined"); + + response = json_stream_success(cmd); + return command_success(cmd, response); +} + +static const struct json_command preapprovekeysend_command = { + "preapprovekeysend", + "payment", + json_preapprovekeysend, + "Ask the HSM to preapprove a keysend payment." +}; +AUTODATA(json_command, &preapprovekeysend_command); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 6aff53439..93d624d18 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -258,6 +258,9 @@ bool fromwire_hsmd_sign_bolt12_reply(const void *p UNNEEDED, struct bip340sig *s /* Generated stub for fromwire_hsmd_preapprove_invoice_reply */ bool fromwire_hsmd_preapprove_invoice_reply(const void *p UNNEEDED, bool *approved UNNEEDED) { fprintf(stderr, "fromwire_hsmd_preapprove_invoice_reply called!\n"); abort(); } +/* Generated stub for fromwire_hsmd_preapprove_keysend_reply */ +bool fromwire_hsmd_preapprove_keysend_reply(const void *p UNNEEDED, bool *approved UNNEEDED) +{ fprintf(stderr, "fromwire_hsmd_preapprove_keysend_reply called!\n"); abort(); } /* Generated stub for fromwire_hsmd_sign_commitment_tx_reply */ bool fromwire_hsmd_sign_commitment_tx_reply(const void *p UNNEEDED, struct bitcoin_signature *sig UNNEEDED) { fprintf(stderr, "fromwire_hsmd_sign_commitment_tx_reply called!\n"); abort(); } @@ -783,6 +786,9 @@ u8 *towire_hsmd_sign_bolt12(const tal_t *ctx UNNEEDED, const wirestring *message /* Generated stub for towire_hsmd_preapprove_invoice */ u8 *towire_hsmd_preapprove_invoice(const tal_t *ctx UNNEEDED, const wirestring *invstring UNNEEDED) { fprintf(stderr, "towire_hsmd_preapprove_invoice called!\n"); abort(); } +/* Generated stub for towire_hsmd_preapprove_keysend */ +u8 *towire_hsmd_preapprove_keysend(const tal_t *ctx UNNEEDED, const struct node_id *destination UNNEEDED, const struct sha256 *payment_hash UNNEEDED, struct amount_msat amount UNNEEDED) +{ fprintf(stderr, "towire_hsmd_preapprove_keysend called!\n"); abort(); } /* Generated stub for towire_hsmd_sign_commitment_tx */ u8 *towire_hsmd_sign_commitment_tx(const tal_t *ctx UNNEEDED, const struct node_id *peer_id UNNEEDED, u64 channel_dbid UNNEEDED, const struct bitcoin_tx *tx UNNEEDED, const struct pubkey *remote_funding_key UNNEEDED, u64 commit_num UNNEEDED) { fprintf(stderr, "towire_hsmd_sign_commitment_tx called!\n"); abort(); } diff --git a/plugins/keysend.c b/plugins/keysend.c index c7ab8f74b..87d774d00 100644 --- a/plugins/keysend.c +++ b/plugins/keysend.c @@ -107,6 +107,59 @@ REGISTER_PAYMENT_MODIFIER(keysend, struct keysend_data *, keysend_init, * End of keysend modifier *****************************************************************************/ +/***************************************************************************** + * check_preapprovekeysend + * + * @desc submit the keysend to the HSM for approval, fail the payment if not approved. + * + * This paymod checks the keysend for approval with the HSM, which might: + * - check with the user for specific approval + * - enforce velocity controls + * - automatically approve the keysend (default) + */ + +static struct command_result * +check_preapprovekeysend_allow(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct payment *p) +{ + /* On success, an empty object is returned. */ + payment_continue(p); + return command_still_pending(cmd); +} + +static struct command_result *preapprovekeysend_rpc_failure(struct command *cmd, + const char *buffer, + const jsmntok_t *toks, + struct payment *p) +{ + payment_abort(p, + "Failing payment due to a failed RPC call: %.*s", + toks->end - toks->start, buffer + toks->start); + return command_still_pending(cmd); +} + +static void check_preapprovekeysend_start(void *d UNUSED, struct payment *p) +{ + /* Ask the HSM if the keysend is OK to pay */ + struct out_req *req; + req = jsonrpc_request_start(p->plugin, NULL, "preapprovekeysend", + &check_preapprovekeysend_allow, + &preapprovekeysend_rpc_failure, p); + json_add_node_id(req->js, "destination", p->destination); + json_add_sha256(req->js, "payment_hash", p->payment_hash); + json_add_amount_msat_only(req->js, "amount_msat", p->amount); + (void) send_outreq(p->plugin, req); +} + +REGISTER_PAYMENT_MODIFIER(check_preapprovekeysend, void *, NULL, + check_preapprovekeysend_start); + +/* + * End of check_preapprovekeysend modifier + *****************************************************************************/ + static const char *init(struct plugin *p, const char *buf UNUSED, const jsmntok_t *config UNUSED) { @@ -123,6 +176,7 @@ static const char *init(struct plugin *p, const char *buf UNUSED, struct payment_modifier *pay_mods[] = { &keysend_pay_mod, + &check_preapprovekeysend_pay_mod, &local_channel_hints_pay_mod, &directpay_pay_mod, &shadowroute_pay_mod,