From f29343d740c087e2b7c477386025b82c22341d82 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 13 Sep 2022 09:37:05 -0700 Subject: [PATCH] hsmd: add hsmd_preapprove_invoice and check_preapproveinvoice pay modifier Changelog-added: hsmd: A new message `hsmd_preapprove_invoice` is added. Changelog-added: JSON-RPC: A new command `preapproveinvoice` is added. --- common/jsonrpc_errors.h | 1 + doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-preapproveinvoice.7.md | 51 +++++++++++++++++++++ doc/schemas/preapproveinvoice.request.json | 13 ++++++ doc/schemas/preapproveinvoice.schema.json | 6 +++ hsmd/hsmd.c | 2 + hsmd/hsmd_wire.csv | 8 ++++ hsmd/libhsmd.c | 21 +++++++++ lightningd/invoice.c | 45 ++++++++++++++++++ lightningd/test/run-invoice-select-inchan.c | 6 +++ plugins/libplugin-pay.c | 48 +++++++++++++++++++ plugins/libplugin-pay.h | 1 + plugins/pay.c | 1 + 14 files changed, 205 insertions(+) create mode 100644 doc/lightning-preapproveinvoice.7.md create mode 100644 doc/schemas/preapproveinvoice.request.json create mode 100644 doc/schemas/preapproveinvoice.schema.json diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index d5659a78f..7afd3dd03 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -46,6 +46,7 @@ enum jsonrpc_errcode { PAY_STOPPED_RETRYING = 210, PAY_STATUS_UNEXPECTED = 211, PAY_INVOICE_REQUEST_INVALID = 212, + PAY_INVOICE_PREAPPROVAL_DECLINED = 213, /* `fundchannel` or `withdraw` errors */ FUND_MAX_EXCEEDED = 300, diff --git a/doc/Makefile b/doc/Makefile index b7951a637..53de4115b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -75,6 +75,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-pay.7 \ doc/lightning-parsefeerate.7 \ doc/lightning-plugin.7 \ + doc/lightning-preapproveinvoice.7 \ doc/lightning-recoverchannel.7 \ doc/lightning-reserveinputs.7 \ doc/lightning-sendinvoice.7 \ diff --git a/doc/index.rst b/doc/index.rst index 687e8cb95..4180153c3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -105,6 +105,7 @@ Core Lightning Documentation lightning-pay lightning-ping lightning-plugin + lightning-preapproveinvoice lightning-recoverchannel lightning-reserveinputs lightning-sendcustommsg diff --git a/doc/lightning-preapproveinvoice.7.md b/doc/lightning-preapproveinvoice.7.md new file mode 100644 index 000000000..c39d96dca --- /dev/null +++ b/doc/lightning-preapproveinvoice.7.md @@ -0,0 +1,51 @@ +lightning-preapproveinvoice -- Ask the HSM to preapprove an invoice (low-level) +================================================================== + +SYNOPSIS +-------- + +**preapproveinvoice** *bolt11* + +DESCRIPTION +----------- + +The **preapproveinvoice** RPC command submits the *bolt11* invoice to +the HSM to check that it is approved for payment. + +Generally the **preapproveinvoice** request does not need to be made +explicitly, it is automatically generated as part of a **pay** request. + +By default, the HSM will approve all **preapproveinvoice** requests. + +If a remote signer is being used it might decline an **preapproveinvoice** +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 **preapproveinvoice** request a subsequent +attempt to pay the invoice 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-pay(7) + +RESOURCES +--------- + +Main web site: + +[comment]: # ( SHA256STAMP:735dd61146b04745f1e884037ead662a386fec2c41e2de1a8698d6bb03f63540) diff --git a/doc/schemas/preapproveinvoice.request.json b/doc/schemas/preapproveinvoice.request.json new file mode 100644 index 000000000..7e80a3f23 --- /dev/null +++ b/doc/schemas/preapproveinvoice.request.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "bolt11" + ], + "properties": { + "bolt11": { + "type": "string" + } + } +} diff --git a/doc/schemas/preapproveinvoice.schema.json b/doc/schemas/preapproveinvoice.schema.json new file mode 100644 index 000000000..1aad2dcae --- /dev/null +++ b/doc/schemas/preapproveinvoice.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 2bc775704..59aa65bad 100644 --- a/hsmd/hsmd.c +++ b/hsmd/hsmd.c @@ -668,6 +668,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_SIGN_MESSAGE: case WIRE_HSMD_SIGN_OPTION_WILL_FUND_OFFER: case WIRE_HSMD_SIGN_BOLT12: + case WIRE_HSMD_PREAPPROVE_INVOICE: case WIRE_HSMD_ECDH_REQ: case WIRE_HSMD_CHECK_FUTURE_SECRET: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY: @@ -708,6 +709,7 @@ static struct io_plan *handle_client(struct io_conn *conn, struct client *c) case WIRE_HSMD_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: + case WIRE_HSMD_PREAPPROVE_INVOICE_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 e294fba4d..0cb4c3e75 100644 --- a/hsmd/hsmd_wire.csv +++ b/hsmd/hsmd_wire.csv @@ -113,6 +113,14 @@ msgdata,hsmd_sign_invoice,hrp,u8,hrplen msgtype,hsmd_sign_invoice_reply,108 msgdata,hsmd_sign_invoice_reply,sig,secp256k1_ecdsa_recoverable_signature, +# Preapprove an invoice for payment +msgtype,hsmd_preapprove_invoice,38 +msgdata,hsmd_preapprove_invoice,invstring,wirestring, + +# Result is true if approved, declined if false +msgtype,hsmd_preapprove_invoice_reply,138 +msgdata,hsmd_preapprove_invoice_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 3ac8452d3..21ab38b5f 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -119,6 +119,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_SIGN_MESSAGE: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY: case WIRE_HSMD_SIGN_BOLT12: + case WIRE_HSMD_PREAPPROVE_INVOICE: case WIRE_HSMD_DERIVE_SECRET: return (client->capabilities & HSM_CAP_MASTER) != 0; @@ -149,6 +150,7 @@ bool hsmd_check_client_capabilities(struct hsmd_client *client, case WIRE_HSMD_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: + case WIRE_HSMD_PREAPPROVE_INVOICE_REPLY: case WIRE_HSMD_DERIVE_SECRET_REPLY: break; } @@ -659,6 +661,22 @@ static u8 *handle_sign_bolt12(struct hsmd_client *c, const u8 *msg_in) return towire_hsmd_sign_bolt12_reply(NULL, &sig); } +/*~ lightningd asks us to approve an invoice. This stub implementation + * is overriden by fully validating signers that need to track invoice + * payments. */ +static u8 *handle_preapprove_invoice(struct hsmd_client *c, const u8 *msg_in) +{ + char *invstring; + bool approved; + if (!fromwire_hsmd_preapprove_invoice(tmpctx, msg_in, &invstring)) + return hsmd_status_malformed_request(c, msg_in); + + /* This stub always approves */ + approved = true; + + return towire_hsmd_preapprove_invoice_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 @@ -1572,6 +1590,8 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, return handle_sign_option_will_fund_offer(client, msg); case WIRE_HSMD_SIGN_BOLT12: return handle_sign_bolt12(client, msg); + case WIRE_HSMD_PREAPPROVE_INVOICE: + return handle_preapprove_invoice(client, msg); case WIRE_HSMD_SIGN_MESSAGE: return handle_sign_message(client, msg); case WIRE_HSMD_GET_CHANNEL_BASEPOINTS: @@ -1635,6 +1655,7 @@ u8 *hsmd_handle_client_message(const tal_t *ctx, struct hsmd_client *client, case WIRE_HSMD_SIGN_MESSAGE_REPLY: case WIRE_HSMD_GET_OUTPUT_SCRIPTPUBKEY_REPLY: case WIRE_HSMD_SIGN_BOLT12_REPLY: + case WIRE_HSMD_PREAPPROVE_INVOICE_REPLY: break; } return hsmd_status_bad_request(client, msg, "Unknown request"); diff --git a/lightningd/invoice.c b/lightningd/invoice.c index ebc98d10c..26a43ab1f 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -1808,3 +1808,48 @@ static const struct json_command createinvoice_command = { }; AUTODATA(json_command, &createinvoice_command); + +static struct command_result *json_preapproveinvoice(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + const char *invstring; + struct json_stream *response; + bool approved; + + if (!param(cmd, buffer, params, + /* FIXME: parameter should be invstring now */ + p_req("bolt11", param_string, &invstring), + NULL)) + return command_param_failed(); + + /* Strip optional URI preamble. */ + if (strncmp(invstring, "lightning:", 10) == 0 || + strncmp(invstring, "LIGHTNING:", 10) == 0) + invstring += 10; + + u8 *msg = towire_hsmd_preapprove_invoice(NULL, invstring); + + 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_invoice_reply(msg, &approved)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "HSM gave bad preapprove_invoice_reply %s", tal_hex(msg, msg)); + + if (!approved) + return command_fail(cmd, PAY_INVOICE_PREAPPROVAL_DECLINED, "invoice was declined"); + + response = json_stream_success(cmd); + return command_success(cmd, response); +} + +static const struct json_command preapproveinvoice_command = { + "preapproveinvoice", + "payment", + json_preapproveinvoice, + "Ask the HSM to preapprove an invoice." +}; +AUTODATA(json_command, &preapproveinvoice_command); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 7ff51c79b..6aff53439 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -255,6 +255,9 @@ bool fromwire_dualopend_dev_memleak_reply(const void *p UNNEEDED, bool *leak UNN /* Generated stub for fromwire_hsmd_sign_bolt12_reply */ bool fromwire_hsmd_sign_bolt12_reply(const void *p UNNEEDED, struct bip340sig *sig UNNEEDED) { fprintf(stderr, "fromwire_hsmd_sign_bolt12_reply called!\n"); abort(); } +/* 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_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(); } @@ -777,6 +780,9 @@ u8 *towire_gossipd_discovered_ip(const tal_t *ctx UNNEEDED, const struct wireadd /* Generated stub for towire_hsmd_sign_bolt12 */ u8 *towire_hsmd_sign_bolt12(const tal_t *ctx UNNEEDED, const wirestring *messagename UNNEEDED, const wirestring *fieldname UNNEEDED, const struct sha256 *merkleroot UNNEEDED, const u8 *publictweak UNNEEDED) { fprintf(stderr, "towire_hsmd_sign_bolt12 called!\n"); abort(); } +/* 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_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/libplugin-pay.c b/plugins/libplugin-pay.c index 5425811ed..41951f961 100644 --- a/plugins/libplugin-pay.c +++ b/plugins/libplugin-pay.c @@ -3905,6 +3905,54 @@ static void payee_incoming_limit_step_cb(void *d UNUSED, struct payment *p) REGISTER_PAYMENT_MODIFIER(payee_incoming_limit, void *, NULL, payee_incoming_limit_step_cb); +/***************************************************************************** + * check_preapproveinvoice + * + * @desc submit the invoice to the HSM for approval, fail the payment if not approved. + * + * This paymod checks the invoice for approval with the HSM, which might: + * - check with the user for specific approval + * - enforce velocity controls + * - automatically approve the invoice (default) + */ + +static struct command_result * +check_preapproveinvoice_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 *preapproveinvoice_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_preapproveinvoice_start(void *d UNUSED, struct payment *p) +{ + /* Ask the HSM if the invoice is OK to pay */ + struct out_req *req; + req = jsonrpc_request_start(p->plugin, NULL, "preapproveinvoice", + &check_preapproveinvoice_allow, + &preapproveinvoice_rpc_failure, p); + /* FIXME: rename parameter to invstring */ + json_add_string(req->js, "bolt11", p->invstring); + (void) send_outreq(p->plugin, req); +} + +REGISTER_PAYMENT_MODIFIER(check_preapproveinvoice, void *, NULL, + check_preapproveinvoice_start); + static struct route_exclusions_data * route_exclusions_data_init(struct payment *p) { diff --git a/plugins/libplugin-pay.h b/plugins/libplugin-pay.h index 94444d0a4..e30142074 100644 --- a/plugins/libplugin-pay.h +++ b/plugins/libplugin-pay.h @@ -448,6 +448,7 @@ REGISTER_PAYMENT_MODIFIER_HEADER(local_channel_hints, void); * each of those channels can bear. */ REGISTER_PAYMENT_MODIFIER_HEADER(payee_incoming_limit, void); REGISTER_PAYMENT_MODIFIER_HEADER(route_exclusions, struct route_exclusions_data); +REGISTER_PAYMENT_MODIFIER_HEADER(check_preapproveinvoice, void); struct payment *payment_new(tal_t *ctx, struct command *cmd, diff --git a/plugins/pay.c b/plugins/pay.c index e5392efbb..268e4fca1 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -928,6 +928,7 @@ payment_listsendpays_previous(struct command *cmd, const char *buf, } struct payment_modifier *paymod_mods[] = { + &check_preapproveinvoice_pay_mod, /* NOTE: The order in which these four paymods are executed is * significant! * local_channel_hints *must* execute first before route_exclusions