Skip to content

Commit 1a1c981

Browse files
Extract payer key derivation helpers for reuse
Extract verify_payer_metadata's core logic into a shared verify_payer_metadata_inner in signer.rs so it can be reused by both the existing verify_payer_metadata (returns PaymentId) and a new derive_payer_keys (returns Keypair). Add Bolt12Invoice::derive_signing_keys which re-derives the payer's signing keypair from ExpandedKey, Nonce, and PaymentId using the same derivation scheme as invoice requests created with deriving_signing_pubkey. This will be used by payer proofs to sign without requiring the caller to hold the raw keypair. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a7cfbfb commit 1a1c981

2 files changed

Lines changed: 109 additions & 6 deletions

File tree

lightning/src/offers/invoice.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,24 @@ impl Bolt12Invoice {
10321032
)
10331033
}
10341034

1035+
/// Re-derives the payer's signing keypair for payer proof creation.
1036+
///
1037+
/// This performs the same key derivation that occurs during invoice request creation
1038+
/// with `deriving_signing_pubkey`, allowing the payer to recover their signing keypair.
1039+
/// The `nonce` and `payment_id` must be the same ones used when creating the original
1040+
/// invoice request (available from [`OffersContext::OutboundPaymentForOffer`]).
1041+
///
1042+
/// [`OffersContext::OutboundPaymentForOffer`]: crate::blinded_path::message::OffersContext::OutboundPaymentForOffer
1043+
pub(crate) fn derive_signing_keys<T: secp256k1::Signing>(
1044+
&self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>,
1045+
) -> Result<Keypair, ()> {
1046+
let iv_bytes = match &self.contents {
1047+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1048+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA,
1049+
};
1050+
self.contents.derive_signing_keys(&self.bytes, payment_id, nonce, key, iv_bytes, secp_ctx)
1051+
}
1052+
10351053
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10361054
let (
10371055
payer_tlv_stream,
@@ -1342,6 +1360,36 @@ impl InvoiceContents {
13421360
)
13431361
}
13441362

1363+
fn derive_signing_keys<T: secp256k1::Signing>(
1364+
&self, bytes: &[u8], payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey,
1365+
iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1<T>,
1366+
) -> Result<Keypair, ()> {
1367+
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
1368+
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
1369+
1370+
let offer_records = TlvStream::new(bytes).range(OFFER_TYPES);
1371+
let invreq_records = TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(|record| {
1372+
match record.r#type {
1373+
PAYER_METADATA_TYPE => false,
1374+
INVOICE_REQUEST_PAYER_ID_TYPE => false,
1375+
_ => true,
1376+
}
1377+
});
1378+
let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES);
1379+
let tlv_stream = offer_records.chain(invreq_records).chain(experimental_records);
1380+
1381+
let signing_pubkey = self.payer_signing_pubkey();
1382+
signer::derive_payer_keys(
1383+
payment_id,
1384+
nonce,
1385+
key,
1386+
iv_bytes,
1387+
signing_pubkey,
1388+
tlv_stream,
1389+
secp_ctx,
1390+
)
1391+
}
1392+
13451393
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef<'_> {
13461394
let (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) =
13471395
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)