Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
65 changes: 65 additions & 0 deletions common/bolt12_contact.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include "config.h"
#include <ccan/tal/tal.h>
#include <common/bolt12_contact.h>
#include <common/utils.h>
#include <wire/bolt12_wiregen.h>
#include <wire/onion_wiregen.h>

/* 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;
}
41 changes: 41 additions & 0 deletions common/bolt12_contact.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef LIGHTNING_COMMON_BOLT12_CONTACT_H
#define LIGHTNING_COMMON_BOLT12_CONTACT_H
#include "config.h"
#include <bitcoin/privkey.h>
#include <bitcoin/pubkey.h>
#include <ccan/crypto/sha256/sha256.h>

/**
* 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 */
11 changes: 11 additions & 0 deletions common/test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
110 changes: 110 additions & 0 deletions common/test/run-bolt12_contact.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "config.h"
#include <stdio.h>
#include "../bolt12_contact.c"
#include <assert.h>
#include <ccan/str/hex/hex.h>
#include <common/setup.h>

/* 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;
}
22 changes: 22 additions & 0 deletions plugins/fetchinvoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <ccan/mem/mem.h>
#include <ccan/str/hex/hex.h>
#include <ccan/tal/str/str.h>
#include <common/bolt12_contact.h>
#include <common/bolt12_id.h>
#include <common/bolt12_merkle.h>
#include <common/clock_time.h>
Expand Down Expand Up @@ -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),
Expand All @@ -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))
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions plugins/offers_invreq_hook.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 8 additions & 0 deletions wire/bolt12_wire.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading