Skip to content

Commit 1705c7a

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 144a24b commit 1705c7a

2 files changed

Lines changed: 143 additions & 21 deletions

File tree

lightning/src/offers/invoice.rs

Lines changed: 82 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::{
@@ -140,13 +141,15 @@ use crate::offers::offer::{
140141
};
141142
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
142143
use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef, PAYER_METADATA_TYPE};
144+
use crate::offers::payer_proof::{PayerProofBuilder, PayerProofError};
143145
use crate::offers::refund::{
144146
Refund, RefundContents, IV_BYTES_WITHOUT_METADATA as REFUND_IV_BYTES_WITHOUT_METADATA,
145147
IV_BYTES_WITH_METADATA as REFUND_IV_BYTES_WITH_METADATA,
146148
};
147149
use crate::offers::signer::{self, Metadata};
148150
use crate::types::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures};
149151
use crate::types::payment::PaymentHash;
152+
use crate::types::payment::PaymentPreimage;
150153
use crate::types::string::PrintableString;
151154
use crate::util::ser::{
152155
CursorReadable, HighZeroBytesDroppedBigSize, Iterable, LengthLimitedRead, LengthReadable,
@@ -1032,6 +1035,42 @@ impl Bolt12Invoice {
10321035
)
10331036
}
10341037

1038+
/// Creates a [`PayerProofBuilder`] for this invoice using the given payment preimage.
1039+
///
1040+
/// Returns an error if the preimage doesn't match the invoice's payment hash.
1041+
///
1042+
/// [`PayerProofBuilder`]: crate::offers::payer_proof::PayerProofBuilder
1043+
pub fn payer_proof_builder(
1044+
&self, preimage: PaymentPreimage,
1045+
) -> Result<PayerProofBuilder<'_>, PayerProofError> {
1046+
PayerProofBuilder::new(self, preimage)
1047+
}
1048+
1049+
/// Re-derives the payer's signing keypair for payer proof creation.
1050+
///
1051+
/// This performs the same key derivation that occurs during invoice request creation
1052+
/// with `deriving_signing_pubkey`, allowing the payer to recover their signing keypair.
1053+
/// The `nonce` and `payment_id` must be the same ones used when creating the original
1054+
/// invoice request (available from [`OffersContext::OutboundPaymentForOffer`]).
1055+
///
1056+
/// [`OffersContext::OutboundPaymentForOffer`]: crate::blinded_path::message::OffersContext::OutboundPaymentForOffer
1057+
pub(crate) fn derive_payer_signing_keys<T: secp256k1::Signing>(
1058+
&self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>,
1059+
) -> Result<Keypair, ()> {
1060+
let iv_bytes = match &self.contents {
1061+
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
1062+
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA,
1063+
};
1064+
self.contents.derive_payer_signing_keys(
1065+
&self.bytes,
1066+
payment_id,
1067+
nonce,
1068+
key,
1069+
iv_bytes,
1070+
secp_ctx,
1071+
)
1072+
}
1073+
10351074
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10361075
let (
10371076
payer_tlv_stream,
@@ -1317,20 +1356,8 @@ impl InvoiceContents {
13171356
&self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
13181357
secp_ctx: &Secp256k1<T>,
13191358
) -> 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-
1359+
let exclude_payer_id = metadata.derives_payer_keys();
1360+
let tlv_stream = Self::payer_tlv_stream(bytes, exclude_payer_id);
13341361
let signing_pubkey = self.payer_signing_pubkey();
13351362
signer::verify_payer_metadata(
13361363
metadata.as_ref(),
@@ -1342,6 +1369,46 @@ impl InvoiceContents {
13421369
)
13431370
}
13441371

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