Skip to content

Commit 02a99b9

Browse files
refactor(offers): extract payer key derivation helpers
Move the invoice/refund payer key derivation logic into reusable helpers so payer proofs can derive the same signing keys without duplicating the metadata and signer flow.
1 parent 144a24b commit 02a99b9

File tree

2 files changed

+130
-21
lines changed

2 files changed

+130
-21
lines changed

lightning/src/offers/invoice.rs

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ use crate::offers::invoice_request::{
131131
IV_BYTES as INVOICE_REQUEST_IV_BYTES,
132132
};
133133
use crate::offers::merkle::{
134-
self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream,
134+
self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvRecord,
135+
TlvStream,
135136
};
136137
use crate::offers::nonce::Nonce;
137138
use crate::offers::offer::{
@@ -1032,6 +1033,31 @@ impl Bolt12Invoice {
10321033
)
10331034
}
10341035

1036+
/// Re-derives the payer's signing keypair for payer proof creation.
1037+
///
1038+
/// This performs the same key derivation that occurs during invoice request creation
1039+
/// with `deriving_signing_pubkey`, allowing the payer to recover their signing keypair.
1040+
/// The `nonce` and `payment_id` must be the same ones used when creating the original
1041+
/// invoice request (available from [`OffersContext::OutboundPaymentForOffer`]).
1042+
///
1043+
/// [`OffersContext::OutboundPaymentForOffer`]: crate::blinded_path::message::OffersContext::OutboundPaymentForOffer
1044+
pub(crate) fn derive_payer_signing_keys<T: secp256k1::Signing>(
1045+
&self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>,
1046+
) -> Result<Keypair, ()> {
1047+
let iv_bytes = match &self.contents {
1048+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1049+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA,
1050+
};
1051+
self.contents.derive_payer_signing_keys(
1052+
&self.bytes,
1053+
payment_id,
1054+
nonce,
1055+
key,
1056+
iv_bytes,
1057+
secp_ctx,
1058+
)
1059+
}
1060+
10351061
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10361062
let (
10371063
payer_tlv_stream,
@@ -1317,20 +1343,8 @@ impl InvoiceContents {
13171343
&self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
13181344
secp_ctx: &Secp256k1<T>,
13191345
) -> Result<PaymentId, ()> {
1320-
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
1321-
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
1322-
1323-
let offer_records = TlvStream::new(bytes).range(OFFER_TYPES);
1324-
let invreq_records = TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(|record| {
1325-
match record.r#type {
1326-
PAYER_METADATA_TYPE => false, // Should be outside range
1327-
INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(),
1328-
_ => true,
1329-
}
1330-
});
1331-
let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES);
1332-
let tlv_stream = offer_records.chain(invreq_records).chain(experimental_records);
1333-
1346+
let exclude_payer_id = metadata.derives_payer_keys();
1347+
let tlv_stream = Self::payer_tlv_stream(bytes, exclude_payer_id);
13341348
let signing_pubkey = self.payer_signing_pubkey();
13351349
signer::verify_payer_metadata(
13361350
metadata.as_ref(),
@@ -1342,6 +1356,46 @@ impl InvoiceContents {
13421356
)
13431357
}
13441358

1359+
fn derive_payer_signing_keys<T: secp256k1::Signing>(
1360+
&self, bytes: &[u8], payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey,
1361+
iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1<T>,
1362+
) -> Result<Keypair, ()> {
1363+
let tlv_stream = Self::payer_tlv_stream(bytes, true);
1364+
let signing_pubkey = self.payer_signing_pubkey();
1365+
signer::derive_payer_keys(
1366+
payment_id,
1367+
nonce,
1368+
key,
1369+
iv_bytes,
1370+
signing_pubkey,
1371+
tlv_stream,
1372+
secp_ctx,
1373+
)
1374+
}
1375+
1376+
/// Builds the TLV stream used for payer metadata verification and key derivation.
1377+
///
1378+
/// When `exclude_payer_id` is true, the payer signing pubkey (type 88) is excluded
1379+
/// from the stream, which is needed when deriving payer keys.
1380+
fn payer_tlv_stream(
1381+
bytes: &[u8], exclude_payer_id: bool,
1382+
) -> impl core::iter::Iterator<Item = TlvRecord<'_>> {
1383+
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
1384+
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
1385+
1386+
let offer_records = TlvStream::new(bytes).range(OFFER_TYPES);
1387+
let invreq_records =
1388+
TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(move |record| {
1389+
match record.r#type {
1390+
PAYER_METADATA_TYPE => false,
1391+
INVOICE_REQUEST_PAYER_ID_TYPE => !exclude_payer_id,
1392+
_ => true,
1393+
}
1394+
});
1395+
let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES);
1396+
offer_records.chain(invreq_records).chain(experimental_records)
1397+
}
1398+
13451399
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef<'_> {
13461400
let (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) =
13471401
match self {

lightning/src/offers/signer.rs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,38 @@ pub(super) fn derive_keys(nonce: Nonce, expanded_key: &ExpandedKey) -> Keypair {
321321
Keypair::from_secret_key(&secp_ctx, &privkey)
322322
}
323323

324+
/// Re-derives the payer signing keypair from the given components.
325+
///
326+
/// This re-performs the same key derivation that occurs during invoice request creation with
327+
/// [`InvoiceRequestBuilder::deriving_signing_pubkey`], allowing the payer to recover their
328+
/// signing keypair for creating payer proofs.
329+
///
330+
/// The `tlv_stream` must contain the offer and invoice request TLV records (excluding
331+
/// payer metadata type 0 and payer_id type 88), matching what was used during
332+
/// the original key derivation.
333+
///
334+
/// [`InvoiceRequestBuilder::deriving_signing_pubkey`]: crate::offers::invoice_request::InvoiceRequestBuilder
335+
pub(super) fn derive_payer_keys<'a, T: secp256k1::Signing>(
336+
payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
337+
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
338+
secp_ctx: &Secp256k1<T>,
339+
) -> Result<Keypair, ()> {
340+
let metadata = Metadata::payer_data(payment_id, nonce, expanded_key);
341+
let metadata_ref = metadata.as_ref();
342+
343+
match verify_payer_metadata_inner(
344+
metadata_ref,
345+
expanded_key,
346+
iv_bytes,
347+
signing_pubkey,
348+
tlv_stream,
349+
secp_ctx,
350+
)? {
351+
Some(keys) => Ok(keys),
352+
None => Err(()),
353+
}
354+
}
355+
324356
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
325357
/// - a 256-bit [`PaymentId`],
326358
/// - a 128-bit [`Nonce`], and possibly
@@ -339,6 +371,34 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>(
339371
return Err(());
340372
}
341373

374+
verify_payer_metadata_inner(
375+
metadata,
376+
expanded_key,
377+
iv_bytes,
378+
signing_pubkey,
379+
tlv_stream,
380+
secp_ctx,
381+
)?;
382+
383+
let mut encrypted_payment_id = [0u8; PaymentId::LENGTH];
384+
encrypted_payment_id.copy_from_slice(&metadata[..PaymentId::LENGTH]);
385+
let nonce = Nonce::try_from(&metadata[PaymentId::LENGTH..][..Nonce::LENGTH]).unwrap();
386+
let payment_id = expanded_key.crypt_for_offer(encrypted_payment_id, nonce);
387+
388+
Ok(PaymentId(payment_id))
389+
}
390+
391+
/// Shared core of [`verify_payer_metadata`] and [`derive_payer_keys`].
392+
///
393+
/// Builds the payer HMAC from the given metadata and TLV stream, then verifies it against the
394+
/// `signing_pubkey`. The `metadata` must be at least `PaymentId::LENGTH` bytes, with the first
395+
/// `PaymentId::LENGTH` bytes being the encrypted payment ID and the remainder being the nonce
396+
/// (and possibly an HMAC).
397+
fn verify_payer_metadata_inner<'a, T: secp256k1::Signing>(
398+
metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
399+
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
400+
secp_ctx: &Secp256k1<T>,
401+
) -> Result<Option<Keypair>, ()> {
342402
let mut encrypted_payment_id = [0u8; PaymentId::LENGTH];
343403
encrypted_payment_id.copy_from_slice(&metadata[..PaymentId::LENGTH]);
344404

@@ -352,12 +412,7 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>(
352412
Hmac::from_engine(hmac),
353413
signing_pubkey,
354414
secp_ctx,
355-
)?;
356-
357-
let nonce = Nonce::try_from(&metadata[PaymentId::LENGTH..][..Nonce::LENGTH]).unwrap();
358-
let payment_id = expanded_key.crypt_for_offer(encrypted_payment_id, nonce);
359-
360-
Ok(PaymentId(payment_id))
415+
)
361416
}
362417

363418
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:

0 commit comments

Comments
 (0)