Skip to content

Commit e984781

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 7ec5f0e commit e984781

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 as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
@@ -824,15 +899,18 @@ mod tests {
824899
use bitcoin::util::schnorr::TweakedPublicKey;
825900
use core::convert::TryFrom;
826901
use core::time::Duration;
827-
use crate::ln::msgs::DecodeError;
902+
use crate::chain::keysinterface::KeyMaterial;
828903
use crate::ln::features::Bolt12InvoiceFeatures;
904+
use crate::ln::inbound_payment::ExpandedKey;
905+
use crate::ln::msgs::DecodeError;
829906
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
830907
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
831908
use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity};
832909
use crate::offers::parse::{ParseError, SemanticError};
833910
use crate::offers::payer::PayerTlvStreamRef;
834911
use crate::offers::refund::RefundBuilder;
835912
use crate::offers::test_utils::*;
913+
use crate::onion_message::{BlindedHop, BlindedPath};
836914
use crate::util::ser::{BigSize, Iterable, Writeable};
837915

838916
trait ToBytes {
@@ -1077,6 +1155,67 @@ mod tests {
10771155
}
10781156
}
10791157

1158+
#[test]
1159+
fn builds_invoice_from_offer_using_derived_keys() {
1160+
let desc = "foo".to_string();
1161+
let node_id = recipient_pubkey();
1162+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
1163+
let entropy = FixedEntropy {};
1164+
let secp_ctx = Secp256k1::new();
1165+
1166+
let blinded_path = BlindedPath {
1167+
introduction_node_id: pubkey(40),
1168+
blinding_point: pubkey(41),
1169+
blinded_hops: vec![
1170+
BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
1171+
BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
1172+
],
1173+
};
1174+
1175+
let offer = OfferBuilder
1176+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
1177+
.amount_msats(1000)
1178+
.path(blinded_path)
1179+
.build().unwrap();
1180+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1181+
.build().unwrap()
1182+
.sign(payer_sign).unwrap();
1183+
1184+
if let Err(e) = invoice_request
1185+
.verify_and_respond_using_derived_keys_no_std(
1186+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1187+
)
1188+
.unwrap()
1189+
.build_and_sign(&secp_ctx)
1190+
{
1191+
panic!("error building invoice: {:?}", e);
1192+
}
1193+
1194+
let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32]));
1195+
match invoice_request.verify_and_respond_using_derived_keys_no_std(
1196+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1197+
) {
1198+
Ok(_) => panic!("expected error"),
1199+
Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
1200+
}
1201+
1202+
let desc = "foo".to_string();
1203+
let offer = OfferBuilder
1204+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
1205+
.amount_msats(1000)
1206+
.build().unwrap();
1207+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1208+
.build().unwrap()
1209+
.sign(payer_sign).unwrap();
1210+
1211+
match invoice_request.verify_and_respond_using_derived_keys_no_std(
1212+
payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
1213+
) {
1214+
Ok(_) => panic!("expected error"),
1215+
Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
1216+
}
1217+
}
1218+
10801219
#[test]
10811220
fn builds_invoice_with_relative_expiry() {
10821221
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};
@@ -466,7 +466,7 @@ impl InvoiceRequest {
466466
#[cfg(feature = "std")]
467467
pub fn respond_with(
468468
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash
469-
) -> Result<InvoiceBuilder, SemanticError> {
469+
) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
470470
let created_at = std::time::SystemTime::now()
471471
.duration_since(std::time::SystemTime::UNIX_EPOCH)
472472
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
@@ -494,19 +494,66 @@ impl InvoiceRequest {
494494
pub fn respond_with_no_std(
495495
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
496496
created_at: core::time::Duration
497-
) -> Result<InvoiceBuilder, SemanticError> {
497+
) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
498498
if self.features().requires_unknown_bits() {
499499
return Err(SemanticError::UnknownRequiredFeatures);
500500
}
501501

502502
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
503503
}
504504

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

512559
#[cfg(test)]

0 commit comments

Comments
 (0)