Skip to content

Commit a64af3d

Browse files
committed
Extract keys from Offer::metadata to sign Invoice
For offers where the signing pubkey is derived, the keys need to be extracted from the Offer::metadata in order to sign an invoice. Parameterize InvoiceBuilder such that a build_and_sign method is available for this situation.
1 parent c686c23 commit a64af3d

File tree

6 files changed

+279
-55
lines changed

6 files changed

+279
-55
lines changed

lightning/src/offers/invoice.rs

Lines changed: 162 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ use bitcoin::blockdata::constants::ChainHash;
9797
use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
9898
use bitcoin::hashes::Hash;
9999
use bitcoin::network::constants::Network;
100-
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
100+
use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self};
101101
use bitcoin::secp256k1::schnorr::Signature;
102102
use bitcoin::util::address::{Address, Payload, WitnessVersion};
103103
use bitcoin::util::schnorr::TweakedPublicKey;
104-
use core::convert::TryFrom;
104+
use core::convert::{Infallible, TryFrom};
105105
use core::time::Duration;
106106
use crate::io;
107107
use crate::ln::PaymentHash;
@@ -136,28 +136,31 @@ pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "
136136
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
137137
/// [`Refund`]: crate::offers::refund::Refund
138138
/// [module-level documentation]: self
139-
pub struct InvoiceBuilder<'a> {
139+
pub struct InvoiceBuilder<'a, S: SigningPubkeyStrategy> {
140140
invreq_bytes: &'a Vec<u8>,
141141
invoice: InvoiceContents,
142+
keys: Option<KeyPair>,
143+
signing_pubkey_strategy: core::marker::PhantomData<S>,
142144
}
143145

144-
impl<'a> InvoiceBuilder<'a> {
146+
/// Indicates how [`Invoice::signing_pubkey`] was set.
147+
pub trait SigningPubkeyStrategy {}
148+
149+
/// [`Invoice::signing_pubkey`] was explicitly set.
150+
pub struct ExplicitSigningPubkey {}
151+
152+
/// [`Invoice::signing_pubkey`] was derived.
153+
pub struct DerivedSigningPubkey {}
154+
155+
impl SigningPubkeyStrategy for ExplicitSigningPubkey {}
156+
impl SigningPubkeyStrategy for DerivedSigningPubkey {}
157+
158+
impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
145159
pub(super) fn for_offer(
146160
invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
147161
created_at: Duration, payment_hash: PaymentHash
148162
) -> Result<Self, SemanticError> {
149-
let amount_msats = match invoice_request.amount_msats() {
150-
Some(amount_msats) => amount_msats,
151-
None => match invoice_request.contents.inner.offer.amount() {
152-
Some(Amount::Bitcoin { amount_msats }) => {
153-
amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1))
154-
.ok_or(SemanticError::InvalidAmount)?
155-
},
156-
Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
157-
None => return Err(SemanticError::MissingAmount),
158-
},
159-
};
160-
163+
let amount_msats = Self::check_amount_msats(invoice_request)?;
161164
let contents = InvoiceContents::ForOffer {
162165
invoice_request: invoice_request.contents.clone(),
163166
fields: InvoiceFields {
@@ -167,7 +170,7 @@ impl<'a> InvoiceBuilder<'a> {
167170
},
168171
};
169172

170-
Self::new(&invoice_request.bytes, contents)
173+
Self::new(&invoice_request.bytes, contents, None)
171174
}
172175

173176
pub(super) fn for_refund(
@@ -183,15 +186,57 @@ impl<'a> InvoiceBuilder<'a> {
183186
},
184187
};
185188

186-
Self::new(&refund.bytes, contents)
189+
Self::new(&refund.bytes, contents, None)
187190
}
191+
}
188192

189-
fn new(invreq_bytes: &'a Vec<u8>, contents: InvoiceContents) -> Result<Self, SemanticError> {
193+
impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
194+
pub(super) fn for_offer_using_keys(
195+
invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
196+
created_at: Duration, payment_hash: PaymentHash, keys: KeyPair
197+
) -> Result<Self, SemanticError> {
198+
let amount_msats = Self::check_amount_msats(invoice_request)?;
199+
let contents = InvoiceContents::ForOffer {
200+
invoice_request: invoice_request.contents.clone(),
201+
fields: InvoiceFields {
202+
payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
203+
fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
204+
signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(),
205+
},
206+
};
207+
208+
Self::new(&invoice_request.bytes, contents, Some(keys))
209+
}
210+
}
211+
212+
impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> {
213+
fn check_amount_msats(invoice_request: &InvoiceRequest) -> Result<u64, SemanticError> {
214+
match invoice_request.amount_msats() {
215+
Some(amount_msats) => Ok(amount_msats),
216+
None => match invoice_request.contents.inner.offer.amount() {
217+
Some(Amount::Bitcoin { amount_msats }) => {
218+
amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1))
219+
.ok_or(SemanticError::InvalidAmount)
220+
},
221+
Some(Amount::Currency { .. }) => Err(SemanticError::UnsupportedCurrency),
222+
None => Err(SemanticError::MissingAmount),
223+
},
224+
}
225+
}
226+
227+
fn new(
228+
invreq_bytes: &'a Vec<u8>, contents: InvoiceContents, keys: Option<KeyPair>
229+
) -> Result<Self, SemanticError> {
190230
if contents.fields().payment_paths.is_empty() {
191231
return Err(SemanticError::MissingPaths);
192232
}
193233

194-
Ok(Self { invreq_bytes, invoice: contents })
234+
Ok(Self {
235+
invreq_bytes,
236+
invoice: contents,
237+
keys,
238+
signing_pubkey_strategy: core::marker::PhantomData,
239+
})
195240
}
196241

197242
/// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
@@ -248,7 +293,9 @@ impl<'a> InvoiceBuilder<'a> {
248293
self.invoice.fields_mut().features.set_basic_mpp_optional();
249294
self
250295
}
296+
}
251297

298+
impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
252299
/// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by
253300
/// [`UnsignedInvoice::sign`].
254301
pub fn build(self) -> Result<UnsignedInvoice<'a>, SemanticError> {
@@ -258,11 +305,36 @@ impl<'a> InvoiceBuilder<'a> {
258305
}
259306
}
260307

261-
let InvoiceBuilder { invreq_bytes, invoice } = self;
308+
let InvoiceBuilder { invreq_bytes, invoice, .. } = self;
262309
Ok(UnsignedInvoice { invreq_bytes, invoice })
263310
}
264311
}
265312

313+
impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
314+
/// Builds a signed [`Invoice`] after checking for valid semantics.
315+
pub fn build_and_sign<T: secp256k1::Signing>(
316+
self, secp_ctx: &Secp256k1<T>
317+
) -> Result<Invoice, SemanticError> {
318+
#[cfg(feature = "std")] {
319+
if self.invoice.is_offer_or_refund_expired() {
320+
return Err(SemanticError::AlreadyExpired);
321+
}
322+
}
323+
324+
let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self;
325+
let keys = match &invoice {
326+
InvoiceContents::ForOffer { .. } => keys.unwrap(),
327+
InvoiceContents::ForRefund { .. } => unreachable!(),
328+
};
329+
330+
let unsigned_invoice = UnsignedInvoice { invreq_bytes, invoice };
331+
let invoice = unsigned_invoice
332+
.sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
333+
.unwrap();
334+
Ok(invoice)
335+
}
336+
}
337+
266338
/// A semantically valid [`Invoice`] that hasn't been signed.
267339
pub struct UnsignedInvoice<'a> {
268340
invreq_bytes: &'a Vec<u8>,
@@ -551,7 +623,10 @@ impl InvoiceContents {
551623
},
552624
};
553625

554-
signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx)
626+
match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) {
627+
Ok(_) => true,
628+
Err(()) => false,
629+
}
555630
}
556631

557632
fn derives_keys(&self) -> bool {
@@ -831,15 +906,18 @@ mod tests {
831906
use bitcoin::util::schnorr::TweakedPublicKey;
832907
use core::convert::TryFrom;
833908
use core::time::Duration;
834-
use crate::ln::msgs::DecodeError;
909+
use crate::chain::keysinterface::KeyMaterial;
835910
use crate::ln::features::Bolt12InvoiceFeatures;
911+
use crate::ln::inbound_payment::ExpandedKey;
912+
use crate::ln::msgs::DecodeError;
836913
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
837914
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
838915
use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity};
839916
use crate::offers::parse::{ParseError, SemanticError};
840917
use crate::offers::payer::PayerTlvStreamRef;
841918
use crate::offers::refund::RefundBuilder;
842919
use crate::offers::test_utils::*;
920+
use crate::onion_message::{BlindedHop, BlindedPath};
843921
use crate::util::ser::{BigSize, Iterable, Writeable};
844922

845923
trait ToBytes {
@@ -1084,6 +1162,67 @@ mod tests {
10841162
}
10851163
}
10861164

1165+
#[test]
1166+
fn builds_invoice_from_offer_using_derived_keys() {
1167+
let desc = "foo".to_string();
1168+
let node_id = recipient_pubkey();
1169+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
1170+
let entropy = FixedEntropy {};
1171+
let secp_ctx = Secp256k1::new();
1172+
1173+
let blinded_path = BlindedPath {
1174+
introduction_node_id: pubkey(40),
1175+
blinding_point: pubkey(41),
1176+
blinded_hops: vec![
1177+
BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
1178+
BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
1179+
],
1180+
};
1181+
1182+
let offer = OfferBuilder
1183+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
1184+
.amount_msats(1000)
1185+
.path(blinded_path)
1186+
.build().unwrap();
1187+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1188+
.build().unwrap()
1189+
.sign(payer_sign).unwrap();
1190+
1191+
if let Err(e) = invoice_request
1192+
.verify_and_respond_using_derived_keys_no_std(
1193+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1194+
)
1195+
.unwrap()
1196+
.build_and_sign(&secp_ctx)
1197+
{
1198+
panic!("error building invoice: {:?}", e);
1199+
}
1200+
1201+
let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32]));
1202+
match invoice_request.verify_and_respond_using_derived_keys_no_std(
1203+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1204+
) {
1205+
Ok(_) => panic!("expected error"),
1206+
Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
1207+
}
1208+
1209+
let desc = "foo".to_string();
1210+
let offer = OfferBuilder
1211+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
1212+
.amount_msats(1000)
1213+
.build().unwrap();
1214+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1215+
.build().unwrap()
1216+
.sign(payer_sign).unwrap();
1217+
1218+
match invoice_request.verify_and_respond_using_derived_keys_no_std(
1219+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1220+
) {
1221+
Ok(_) => panic!("expected error"),
1222+
Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
1223+
}
1224+
}
1225+
10871226
#[test]
10881227
fn builds_invoice_with_relative_expiry() {
10891228
let now = now();

lightning/src/offers/invoice_request.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ use crate::ln::PaymentHash;
6464
use crate::ln::features::InvoiceRequestFeatures;
6565
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
6666
use crate::ln::msgs::DecodeError;
67-
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
68-
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
67+
use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder};
68+
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
6969
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
7070
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
7171
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
@@ -469,7 +469,7 @@ impl InvoiceRequest {
469469
#[cfg(feature = "std")]
470470
pub fn respond_with(
471471
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash
472-
) -> Result<InvoiceBuilder, SemanticError> {
472+
) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
473473
let created_at = std::time::SystemTime::now()
474474
.duration_since(std::time::SystemTime::UNIX_EPOCH)
475475
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
@@ -497,19 +497,66 @@ impl InvoiceRequest {
497497
pub fn respond_with_no_std(
498498
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
499499
created_at: core::time::Duration
500-
) -> Result<InvoiceBuilder, SemanticError> {
500+
) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
501501
if self.features().requires_unknown_bits() {
502502
return Err(SemanticError::UnknownRequiredFeatures);
503503
}
504504

505505
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
506506
}
507507

508-
/// Verifies that the request was for an offer created using the given key.
508+
/// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses
509+
/// derived signing keys from the originating [`Offer`] to sign the [`Invoice`]. Must use the
510+
/// same [`ExpandedKey`] as the one used to create the offer.
511+
///
512+
/// See [`InvoiceRequest::respond_with`] for further details.
513+
///
514+
/// [`Invoice`]: crate::offers::invoice::Invoice
515+
#[cfg(feature = "std")]
516+
pub fn verify_and_respond_using_derived_keys<T: secp256k1::Signing>(
517+
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
518+
expanded_key: &ExpandedKey, secp_ctx: &Secp256k1<T>
519+
) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError> {
520+
let created_at = std::time::SystemTime::now()
521+
.duration_since(std::time::SystemTime::UNIX_EPOCH)
522+
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
523+
524+
self.verify_and_respond_using_derived_keys_no_std(
525+
payment_paths, payment_hash, created_at, expanded_key, secp_ctx
526+
)
527+
}
528+
529+
/// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses
530+
/// derived signing keys from the originating [`Offer`] to sign the [`Invoice`]. Must use the
531+
/// same [`ExpandedKey`] as the one used to create the offer.
532+
///
533+
/// See [`InvoiceRequest::respond_with_no_std`] for further details.
534+
///
535+
/// [`Invoice`]: crate::offers::invoice::Invoice
536+
pub fn verify_and_respond_using_derived_keys_no_std<T: secp256k1::Signing>(
537+
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
538+
created_at: core::time::Duration, expanded_key: &ExpandedKey, secp_ctx: &Secp256k1<T>
539+
) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError> {
540+
if self.features().requires_unknown_bits() {
541+
return Err(SemanticError::UnknownRequiredFeatures);
542+
}
543+
544+
let keys = match self.verify(expanded_key, secp_ctx) {
545+
Err(()) => return Err(SemanticError::InvalidMetadata),
546+
Ok(None) => return Err(SemanticError::InvalidMetadata),
547+
Ok(Some(keys)) => keys,
548+
};
549+
550+
InvoiceBuilder::for_offer_using_keys(self, payment_paths, created_at, payment_hash, keys)
551+
}
552+
553+
/// Verifies that the request was for an offer created using the given key. Returns the derived
554+
/// keys need to sign an [`Invoice`] for the request if they could be extracted from the
555+
/// metadata.
509556
pub fn verify<T: secp256k1::Signing>(
510557
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
511-
) -> bool {
512-
self.contents.inner.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx)
558+
) -> Result<Option<KeyPair>, ()> {
559+
self.contents.inner.offer.verify(&self.bytes, key, secp_ctx)
513560
}
514561

515562
#[cfg(test)]

0 commit comments

Comments
 (0)