Skip to content

Commit df8cd15

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 ed02087 commit df8cd15

2 files changed

Lines changed: 129 additions & 21 deletions

File tree

lightning/src/offers/invoice.rs

Lines changed: 72 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,34 @@ 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+
///
1041+
/// The `nonce` and `payment_id` must be the same ones used when creating the original
1042+
/// invoice request. In the common proof-of-payment flow, callers can instead use
1043+
/// `PaidBolt12Invoice::prove_payer_derived` together with the `payment_id` from
1044+
/// [`Event::PaymentSent`].
1045+
///
1046+
/// [`Event::PaymentSent`]: crate::events::Event::PaymentSent
1047+
pub fn derive_payer_signing_keys<T: secp256k1::Signing>(
1048+
&self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>,
1049+
) -> Result<Keypair, ()> {
1050+
let iv_bytes = match &self.contents {
1051+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1052+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA,
1053+
};
1054+
self.contents.derive_payer_signing_keys(
1055+
&self.bytes,
1056+
payment_id,
1057+
nonce,
1058+
key,
1059+
iv_bytes,
1060+
secp_ctx,
1061+
)
1062+
}
1063+
10351064
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10361065
let (
10371066
payer_tlv_stream,
@@ -1317,20 +1346,8 @@ impl InvoiceContents {
13171346
&self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
13181347
secp_ctx: &Secp256k1<T>,
13191348
) -> 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-
1349+
let exclude_payer_id = metadata.derives_payer_keys();
1350+
let tlv_stream = Self::payer_tlv_stream(bytes, exclude_payer_id);
13341351
let signing_pubkey = self.payer_signing_pubkey();
13351352
signer::verify_payer_metadata(
13361353
metadata.as_ref(),
@@ -1342,6 +1359,46 @@ impl InvoiceContents {
13421359
)
13431360
}
13441361

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

lightning/src/offers/signer.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,34 @@ 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+
/// Performs the same derivation as keys created by [`Metadata::derive_from`] when using
327+
/// [`Metadata::DerivedSigningPubkey`] with a [`MetadataMaterial`] built from a `payment_id`.
328+
///
329+
/// The `tlv_stream` must contain the records matching what was used during the original
330+
/// key derivation.
331+
pub(super) fn derive_payer_keys<'a, T: secp256k1::Signing>(
332+
payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
333+
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
334+
secp_ctx: &Secp256k1<T>,
335+
) -> Result<Keypair, ()> {
336+
let metadata = Metadata::payer_data(payment_id, nonce, expanded_key);
337+
let metadata_ref = metadata.as_ref();
338+
339+
match verify_payer_metadata_inner(
340+
metadata_ref,
341+
expanded_key,
342+
iv_bytes,
343+
signing_pubkey,
344+
tlv_stream,
345+
secp_ctx,
346+
)? {
347+
Some(keys) => Ok(keys),
348+
None => Err(()),
349+
}
350+
}
351+
324352
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
325353
/// - a 256-bit [`PaymentId`],
326354
/// - a 128-bit [`Nonce`], and possibly
@@ -339,6 +367,34 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>(
339367
return Err(());
340368
}
341369

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

@@ -352,12 +408,7 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>(
352408
Hmac::from_engine(hmac),
353409
signing_pubkey,
354410
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))
411+
)
361412
}
362413

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

0 commit comments

Comments
 (0)