diff --git a/common/Makefile b/common/Makefile index 62ceb252a8ba..4bf794466621 100644 --- a/common/Makefile +++ b/common/Makefile @@ -16,6 +16,7 @@ COMMON_SRC_NOGEN := \ common/bolt11.c \ common/bolt11_json.c \ common/bolt12.c \ + common/bolt12_contact.c \ common/bolt12_id.c \ common/bolt12_merkle.c \ common/channel_config.c \ diff --git a/common/bolt12_contact.c b/common/bolt12_contact.c new file mode 100644 index 000000000000..1cb96e7186b1 --- /dev/null +++ b/common/bolt12_contact.c @@ -0,0 +1,65 @@ +#include "config.h" +#include +#include +#include +#include +#include + +/* bLIP 42: + * contact_secret = SHA256("blip42_contact_secret" || shared_key) + * where shared_key = local_offer_privkey * remote_offer_node_id + * serialized as a compressed pubkey (33 bytes). + * + * This matches the LDK reference implementation which uses + * `offer_node_id.mul_tweak(&secp, &scalar)` then `.serialize()`. + */ +bool bolt12_contact_secret(const struct privkey *local_offer_privkey, + const struct pubkey *remote_offer_node_id, + struct sha256 *contact_secret) +{ + secp256k1_pubkey shared_point; + u8 compressed[33]; + size_t compressed_len = sizeof(compressed); + struct sha256_ctx sctx; + static const char tag[] = "blip42_contact_secret"; + + /* EC point multiplication: shared_point = privkey * pubkey */ + shared_point = remote_offer_node_id->pubkey; + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, &shared_point, + local_offer_privkey->secret.data) != 1) + return false; + + /* Serialize as compressed point (33 bytes) */ + secp256k1_ec_pubkey_serialize(secp256k1_ctx, compressed, + &compressed_len, &shared_point, + SECP256K1_EC_COMPRESSED); + + sha256_init(&sctx); + sha256_update(&sctx, tag, strlen(tag)); + sha256_update(&sctx, compressed, compressed_len); + sha256_done(&sctx, contact_secret); + return true; +} + +bool offer_contact_node_id(const struct tlv_offer *offer, + struct pubkey *node_id) +{ + /* bLIP 42: + * - offer_issuer_id if present, otherwise + * - the last blinded_node_id of the first blinded path. + */ + if (offer->offer_issuer_id) { + *node_id = *offer->offer_issuer_id; + return true; + } + + if (offer->offer_paths && tal_count(offer->offer_paths) > 0) { + struct blinded_path *first_path = offer->offer_paths[0]; + size_t num_hops = tal_count(first_path->path); + if (num_hops > 0) { + *node_id = first_path->path[num_hops - 1]->blinded_node_id; + return true; + } + } + return false; +} diff --git a/common/bolt12_contact.h b/common/bolt12_contact.h new file mode 100644 index 000000000000..909cad8161b5 --- /dev/null +++ b/common/bolt12_contact.h @@ -0,0 +1,41 @@ +#ifndef LIGHTNING_COMMON_BOLT12_CONTACT_H +#define LIGHTNING_COMMON_BOLT12_CONTACT_H +#include "config.h" +#include +#include +#include + +/** + * bolt12_contact_secret - Derive the bLIP-42 contact_secret for a pair of offers. + * @local_offer_privkey: our offer's private key (offer_issuer_id privkey, + * or last blinded path privkey). + * @remote_offer_node_id: the remote offer's public key (offer_issuer_id, + * or last blinded_node_id of first path). + * @contact_secret: (out) the derived 32-byte contact secret. + * + * Computes: + * shared_key = ECDH(local_offer_privkey, remote_offer_node_id) + * contact_secret = SHA256("blip42_contact_secret" || shared_key) + * + * Returns false on ECDH failure. + */ +bool bolt12_contact_secret(const struct privkey *local_offer_privkey, + const struct pubkey *remote_offer_node_id, + struct sha256 *contact_secret); + +/** + * offer_node_id - Extract the node_id to use for contact derivation from an offer. + * @offer: the decoded offer TLV. + * @node_id: (out) the extracted public key. + * + * Per bLIP 42, this is: + * - offer_issuer_id if present, otherwise + * - the last blinded_node_id of the first blinded path. + * + * Returns false if neither is available. + */ +struct tlv_offer; +bool offer_contact_node_id(const struct tlv_offer *offer, + struct pubkey *node_id); + +#endif /* LIGHTNING_COMMON_BOLT12_CONTACT_H */ diff --git a/common/test/Makefile b/common/test/Makefile index 988ef151cb85..f8c838eb599e 100644 --- a/common/test/Makefile +++ b/common/test/Makefile @@ -106,6 +106,17 @@ common/test/run-version: \ wire/towire.o +common/test/run-bolt12_contact: \ + common/amount.o \ + common/bigsize.o \ + common/base32.o \ + common/node_id.o \ + common/wireaddr.o \ + wire/bolt12_wiregen.o \ + wire/fromwire.o \ + wire/tlvstream.o \ + wire/towire.o + common/test/run-splice_script: \ common/amount.o \ common/node_id.o \ diff --git a/common/test/run-bolt12_contact.c b/common/test/run-bolt12_contact.c new file mode 100644 index 000000000000..b84aa1c262c0 --- /dev/null +++ b/common/test/run-bolt12_contact.c @@ -0,0 +1,110 @@ +#include "config.h" +#include +#include "../bolt12_contact.c" +#include +#include +#include + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for fromwire_blinded_path */ +struct blinded_path *fromwire_blinded_path(const tal_t *ctx UNNEEDED, const u8 **cursor UNNEEDED, size_t *plen UNNEEDED) +{ fprintf(stderr, "fromwire_blinded_path called!\n"); abort(); } +/* Generated stub for towire_blinded_path */ +void towire_blinded_path(u8 **p UNNEEDED, const struct blinded_path *blinded_path UNNEEDED) +{ fprintf(stderr, "towire_blinded_path called!\n"); abort(); } +/* Generated stub for fromwire_sciddir_or_pubkey */ +void fromwire_sciddir_or_pubkey(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct sciddir_or_pubkey *sciddir_or_pubkey UNNEEDED) +{ fprintf(stderr, "fromwire_sciddir_or_pubkey called!\n"); abort(); } +/* Generated stub for towire_sciddir_or_pubkey */ +void towire_sciddir_or_pubkey(u8 **pptr UNNEEDED, const struct sciddir_or_pubkey *sciddir_or_pubkey UNNEEDED) +{ fprintf(stderr, "towire_sciddir_or_pubkey called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +static void hex_to_privkey(const char *hex, struct privkey *privkey) +{ + assert(hex_decode(hex, strlen(hex), privkey->secret.data, + sizeof(privkey->secret.data))); +} + +static void hex_to_pubkey(const char *hex, struct pubkey *pubkey) +{ + u8 buf[33]; + assert(hex_decode(hex, strlen(hex), buf, sizeof(buf))); + assert(pubkey_from_der(buf, sizeof(buf), pubkey)); +} + +static void hex_to_sha256(const char *hex, struct sha256 *sha) +{ + assert(hex_decode(hex, strlen(hex), sha->u.u8, sizeof(sha->u.u8))); +} + +/* bLIP 42 Test Vector 1: Both offers use blinded paths only */ +static void test_vector_1(void) +{ + struct privkey alice_priv, bob_priv; + struct pubkey alice_pub, bob_pub; + struct sha256 expected, result_alice, result_bob; + + hex_to_privkey("4ed1a01dae275f7b7ba503dbae23dddd774a8d5f64788ef7a768ed647dd0e1eb", + &alice_priv); + hex_to_pubkey("0284c9c6f04487ac22710176377680127dfcf110aa0fa8186793c7dd01bafdcfd9", + &alice_pub); + hex_to_privkey("12afb8248c7336e6aea5fe247bc4bac5dcabfb6017bd67b32c8195a6c56b8333", + &bob_priv); + hex_to_pubkey("035e4d1b7237898390e7999b6835ef83cd93b98200d599d29075b45ab0fedc2b34", + &bob_pub); + hex_to_sha256("810641fab614f8bc1441131dc50b132fd4d1e2ccd36f84b887bbab3a6d8cc3d8", + &expected); + + /* Alice derives contact_secret using her privkey and Bob's pubkey */ + assert(bolt12_contact_secret(&alice_priv, &bob_pub, &result_alice)); + assert(sha256_eq(&result_alice, &expected)); + + /* Bob derives the same contact_secret using his privkey and Alice's pubkey */ + assert(bolt12_contact_secret(&bob_priv, &alice_pub, &result_bob)); + assert(sha256_eq(&result_bob, &expected)); + + /* Both sides get the same result */ + assert(sha256_eq(&result_alice, &result_bob)); +} + +/* bLIP 42 Test Vector 2: One offer uses blinded paths and issuer_id */ +static void test_vector_2(void) +{ + struct privkey alice_priv, bob_priv; + struct pubkey alice_pub, bob_pub; + struct sha256 expected, result_alice, result_bob; + + hex_to_privkey("4ed1a01dae275f7b7ba503dbae23dddd774a8d5f64788ef7a768ed647dd0e1eb", + &alice_priv); + hex_to_pubkey("0284c9c6f04487ac22710176377680127dfcf110aa0fa8186793c7dd01bafdcfd9", + &alice_pub); + hex_to_privkey("bcaafa8ed73da11437ce58c7b3458567a870168c0da325a40292fed126b97845", + &bob_priv); + hex_to_pubkey("023f54c2d913e2977c7fc7dfec029750d128d735a39341d8b08d56fb6edf47c8c6", + &bob_pub); + hex_to_sha256("4e0aa72cc42eae9f8dc7c6d2975bbe655683ada2e9abfdfe9f299d391ed9736c", + &expected); + + /* Alice derives using her privkey and Bob's issuer_id pubkey */ + assert(bolt12_contact_secret(&alice_priv, &bob_pub, &result_alice)); + assert(sha256_eq(&result_alice, &expected)); + + /* Bob derives using his privkey and Alice's blinded path pubkey */ + assert(bolt12_contact_secret(&bob_priv, &alice_pub, &result_bob)); + assert(sha256_eq(&result_bob, &expected)); + + /* Both sides get the same result */ + assert(sha256_eq(&result_alice, &result_bob)); +} + +int main(int argc, char *argv[]) +{ + common_setup(argv[0]); + + test_vector_1(); + test_vector_2(); + + common_shutdown(); + return 0; +} diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index 848f3504ad02..95b9d035aa1f 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -911,6 +912,8 @@ struct command_result *json_fetchinvoice(struct command *cmd, u32 *timeout; u64 *quantity; u32 *recurrence_counter, *recurrence_start; + struct sha256 *contact_secret; + struct tlv_offer *contact_offer; if (!param_check(cmd, buffer, params, p_req("offer", param_offer, &sent->offer), @@ -923,6 +926,8 @@ struct command_result *json_fetchinvoice(struct command *cmd, p_opt("payer_note", param_string, &payer_note), p_opt("payer_metadata", param_bin_from_hex, &payer_metadata), p_opt("bip353", param_bip353, &bip353), + p_opt("contact_secret", param_sha256, &contact_secret), + p_opt("contact_offer", param_offer, &contact_offer), p_opt("dev_path_use_scidd", param_dev_scidd, &sent->dev_path_use_scidd), p_opt("dev_reply_path", param_dev_reply_path, &sent->dev_reply_path), NULL)) @@ -1130,6 +1135,23 @@ struct command_result *json_fetchinvoice(struct command *cmd, strlen(payer_note), 0); + /* bLIP 42: If contact_secret is provided, include it and + * optionally include the payer's own offer for pay-back. */ + if (contact_offer && !contact_secret) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "contact_offer requires contact_secret"); + } + if (contact_secret) { + invreq->invreq_contact_secret = tal_dup(invreq, struct sha256, + contact_secret); + } + if (contact_offer) { + u8 *offer_wire = tal_arr(tmpctx, u8, 0); + towire_tlv_offer(&offer_wire, contact_offer); + invreq->invreq_payer_offer = tal_dup_talarr(invreq, u8, + offer_wire); + } + /* If only checking, we're done now */ if (command_check_only(cmd)) return command_check_done(cmd); diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c index ad598daabd63..3ecb017426ee 100644 --- a/plugins/offers_invreq_hook.c +++ b/plugins/offers_invreq_hook.c @@ -1192,6 +1192,20 @@ struct command_result *handle_invoice_request(struct command *cmd, * - MUST reject the invoice request if the offer fields do not exactly match a * valid, unexpired offer. */ + /* bLIP 42: Log incoming contact fields (PoC) */ + if (ir->invreq->invreq_contact_secret) { + plugin_log(cmd->plugin, LOG_INFORM, + "bLIP 42: invoice_request contains contact_secret: %s", + tal_hexstr(tmpctx, + ir->invreq->invreq_contact_secret, + sizeof(*ir->invreq->invreq_contact_secret))); + if (ir->invreq->invreq_payer_offer) { + plugin_log(cmd->plugin, LOG_INFORM, + "bLIP 42: invoice_request contains payer_offer (%zu bytes)", + tal_bytelen(ir->invreq->invreq_payer_offer)); + } + } + invreq_offer_id(ir->invreq, &ir->offer_id); /* Now, look up offer */ diff --git a/wire/bolt12_wire.csv b/wire/bolt12_wire.csv index c67960a9c795..b232f5397558 100644 --- a/wire/bolt12_wire.csv +++ b/wire/bolt12_wire.csv @@ -94,6 +94,10 @@ tlvdata,invoice_request,invreq_recurrence_counter,counter,tu32, tlvtype,invoice_request,invreq_recurrence_start,2000000093 tlvdata,invoice_request,invreq_recurrence_start,period_offset,tu32, tlvtype,invoice_request,invreq_recurrence_cancel,2000000094 +tlvtype,invoice_request,invreq_contact_secret,2000001729 +tlvdata,invoice_request,invreq_contact_secret,secret,sha256, +tlvtype,invoice_request,invreq_payer_offer,2000001731 +tlvdata,invoice_request,invreq_payer_offer,offer_data,byte,... tlvtype,invoice_request,signature,240 tlvdata,invoice_request,signature,sig,bip340sig, subtype,bip_353_name @@ -155,6 +159,10 @@ tlvtype,invoice,invreq_recurrence_counter,2000000092 tlvdata,invoice,invreq_recurrence_counter,counter,tu32, tlvtype,invoice,invreq_recurrence_start,2000000093 tlvdata,invoice,invreq_recurrence_start,period_offset,tu32, +tlvtype,invoice,invreq_contact_secret,2000001729 +tlvdata,invoice,invreq_contact_secret,secret,sha256, +tlvtype,invoice,invreq_payer_offer,2000001731 +tlvdata,invoice,invreq_payer_offer,offer_data,byte,... tlvtype,invoice,invoice_paths,160 tlvdata,invoice,invoice_paths,paths,blinded_path,... tlvtype,invoice,invoice_blindedpay,162