From 3302f25e9fceb7b79632710cd13a789e6d0daee5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 31 Jan 2023 12:52:08 -0600 Subject: [PATCH 01/15] Fix BOLT 12 invoice doctests to work with no-std --- lightning/src/offers/invoice.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index f423677fbc9..f82544913ed 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -45,7 +45,12 @@ //! //! // Invoice for the "offer to be paid" flow. //! InvoiceRequest::try_from(bytes)? -//! .respond_with(payment_paths, payment_hash)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with(payment_paths, payment_hash, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) @@ -69,7 +74,12 @@ //! // Invoice for the "offer for money" flow. //! "lnr1qcp4256ypq" //! .parse::()? -//! .respond_with(payment_paths, payment_hash, pubkey)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash, pubkey)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) From 4763612131e429ec77883ff7e34ce28acb096b26 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 31 Jan 2023 14:35:49 -0600 Subject: [PATCH 02/15] Make separate no-std version for invoice response Both Refund::respond_with and InvoiceRequest::respond_with take a created_at since the Unix epoch Duration in no-std. However, this can cause problems if two downstream dependencies want to use the lightning crate with different feature flags set. Instead, define respond_with_no_std versions of each method in addition to a respond_with version in std. --- lightning/src/offers/invoice.rs | 51 +++++++++++++------------ lightning/src/offers/invoice_request.rs | 33 ++++++++++------ lightning/src/offers/refund.rs | 36 +++++++++++------ 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index f82544913ed..66642dec7b2 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -49,7 +49,7 @@ .respond_with(payment_paths, payment_hash)? ")] #![cfg_attr(not(feature = "std"), doc = " - .respond_with(payment_paths, payment_hash, core::time::Duration::from_secs(0))? + .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? ")] //! .relative_expiry(3600) //! .allow_mpp() @@ -78,7 +78,7 @@ .respond_with(payment_paths, payment_hash, pubkey)? ")] #![cfg_attr(not(feature = "std"), doc = " - .respond_with(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? + .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? ")] //! .relative_expiry(3600) //! .allow_mpp() @@ -886,7 +886,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths.clone(), payment_hash, now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, now).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -962,7 +962,8 @@ mod tests { let now = now(); let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap() - .respond_with(payment_paths.clone(), payment_hash, recipient_pubkey(), now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now) + .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1040,7 +1041,8 @@ mod tests { if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { panic!("error building invoice: {:?}", e); @@ -1049,7 +1051,8 @@ mod tests { match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { Ok(_) => panic!("expected error"), @@ -1068,7 +1071,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now).unwrap() .relative_expiry(one_hour.as_secs() as u32) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1084,7 +1087,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now - one_hour).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour).unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1104,7 +1107,7 @@ mod tests { .amount_msats(1001).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); @@ -1125,7 +1128,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey) @@ -1170,7 +1173,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .allow_mpp() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1187,7 +1190,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(|_| Err(())) { @@ -1201,7 +1204,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(payer_sign) { @@ -1218,7 +1221,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1273,7 +1276,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1303,7 +1306,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .relative_expiry(3600) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1325,7 +1328,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1355,7 +1358,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1383,7 +1386,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .allow_mpp() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1416,7 +1419,7 @@ mod tests { .build().unwrap() .sign(payer_sign).unwrap(); let mut unsigned_invoice = invoice_request - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey) @@ -1473,7 +1476,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1515,7 +1518,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .invoice .write(&mut buffer).unwrap(); @@ -1534,7 +1537,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); let last_signature_byte = invoice.bytes.last_mut().unwrap(); @@ -1559,7 +1562,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 5b15704ca51..b0a8540b2a7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -322,12 +322,30 @@ impl InvoiceRequest { self.signature } + /// Creates an [`Invoice`] for the request with the given required fields and using the + /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. + /// + /// See [`InvoiceRequest::respond_with_no_std`] for further details where the aforementioned + /// creation time is used for the `created_at` parameter. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Duration`]: core::time::Duration + #[cfg(feature = "std")] + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash + ) -> Result { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_with_no_std(payment_paths, payment_hash, created_at) + } + /// Creates an [`Invoice`] for the request with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter - /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] - /// is not available. + /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where + /// [`std::time::SystemTime`] is not available. /// /// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment /// for the invoice. @@ -339,23 +357,16 @@ impl InvoiceRequest { /// /// Errors if the request contains unknown required features. /// - /// [`Duration`]: core::time::Duration /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at - pub fn respond_with( + pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, - #[cfg(any(test, not(feature = "std")))] created_at: core::time::Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(all(not(test), feature = "std"))] - let created_at = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index fff33873954..d64f19db06a 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -317,12 +317,31 @@ impl Refund { self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } + /// Creates an [`Invoice`] for the refund with the given required fields and using the + /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. + /// + /// See [`Refund::respond_with_no_std`] for further details where the aforementioned creation + /// time is used for the `created_at` parameter. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Duration`]: core::time::Duration + #[cfg(feature = "std")] + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + signing_pubkey: PublicKey, + ) -> Result { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_with_no_std(payment_paths, payment_hash, signing_pubkey, created_at) + } + /// Creates an [`Invoice`] for the refund with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter - /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] - /// is not available. + /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where + /// [`std::time::SystemTime`] is not available. /// /// The caller is expected to remember the preimage of `payment_hash` in order to /// claim a payment for the invoice. @@ -339,21 +358,14 @@ impl Refund { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at - pub fn respond_with( + pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, - signing_pubkey: PublicKey, - #[cfg(any(test, not(feature = "std")))] - created_at: Duration + signing_pubkey: PublicKey, created_at: Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(all(not(test), feature = "std"))] - let created_at = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) } From 6388c9a3e52423ae28ce5136307ae18b30186357 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 31 Jan 2023 14:34:04 -0600 Subject: [PATCH 03/15] Add test for requesting invoice from expired offer --- lightning/src/offers/invoice.rs | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 66642dec7b2..ad59bf35b0c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1032,6 +1032,42 @@ mod tests { } } + #[cfg(feature = "std")] + #[test] + fn builds_invoice_from_offer_with_expiration() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(future_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + panic!("error building invoice: {:?}", e); + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(past_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::AlreadyExpired), + } + } + #[cfg(feature = "std")] #[test] fn builds_invoice_from_refund_with_expiration() { From 153d831ccc931683dbd505bdd07c915409e9a9e1 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 19 Jan 2023 10:10:38 -0600 Subject: [PATCH 04/15] Make offers module public This is needed in order to fuzz test BOLT 12 message deserialization. --- lightning/src/lib.rs | 3 +-- lightning/src/offers/invoice.rs | 4 ++-- lightning/src/offers/invoice_request.rs | 2 +- lightning/src/offers/offer.rs | 2 +- lightning/src/offers/refund.rs | 2 +- lightning/src/util/ser_macros.rs | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index d4289d07d2e..71f82ed0134 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -78,8 +78,7 @@ extern crate core; pub mod util; pub mod chain; pub mod ln; -#[allow(unused)] -mod offers; +pub mod offers; pub mod routing; pub mod onion_message; diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ad59bf35b0c..2d4b57d1a24 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -16,7 +16,7 @@ //! The payment recipient must include a [`PaymentHash`], so as to reveal the preimage upon payment //! receipt, and one or more [`BlindedPath`]s for the payer to use when sending the payment. //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate lightning; //! @@ -1462,7 +1462,7 @@ mod tests { .build().unwrap(); // Only standard addresses will be included. - let mut fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); // Non-standard addresses fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] }); fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] }); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index b0a8540b2a7..578b0a2398e 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -19,7 +19,7 @@ //! [`Invoice`]: crate::offers::invoice::Invoice //! [`Refund`]: crate::offers::refund::Refund //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate lightning; //! diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a2008b6a0b5..a535b378250 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -13,7 +13,7 @@ //! published as a QR code to be scanned by a customer. The customer uses the offer to request an //! invoice from the merchant to be paid. //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate core; //! extern crate lightning; diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index d64f19db06a..1488fbe97e1 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -18,7 +18,7 @@ //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest //! [`Offer`]: crate::offers::offer::Offer //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate core; //! extern crate lightning; diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 373a64e3e0e..2dd764022cd 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -378,7 +378,6 @@ macro_rules! decode_tlv_stream_with_custom_tlv_decode { macro_rules! _decode_tlv_stream_range { ($stream: expr, $range: expr, $rewind: ident, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} $(, $decode_custom_tlv: expr)?) => { { - use core::ops::RangeBounds; use $crate::ln::msgs::DecodeError; let mut last_seen_type: Option = None; let mut stream_ref = $stream; @@ -401,7 +400,7 @@ macro_rules! _decode_tlv_stream_range { } }, Err(e) => return Err(e), - Ok(t) => if $range.contains(&t.0) { t } else { + Ok(t) => if core::ops::RangeBounds::contains(&$range, &t.0) { t } else { drop(tracking_reader); // Assumes the type id is minimally encoded, which is enforced on read. From 53d2d473605e29c52d01ddfa625f628be456861c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 20 Jan 2023 12:31:17 -0600 Subject: [PATCH 05/15] Fuzz test for parsing Offer An offer is serialized as a TLV stream and encoded in bech32 without a checksum. Add a fuzz test that parses the unencoded TLV stream and deserializes the underlying Offer. Then compare the original bytes with those obtained by re-serializing the Offer. --- fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/offer_deser_target.rs | 113 +++++++++++++++++++++++++++++ fuzz/src/lib.rs | 1 + fuzz/src/offer_deser.rs | 69 ++++++++++++++++++ fuzz/targets.h | 1 + lightning/src/offers/offer.rs | 4 +- 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 fuzz/src/bin/offer_deser_target.rs create mode 100644 fuzz/src/offer_deser.rs diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index fa29540f96b..f322e60697a 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -9,6 +9,7 @@ GEN_TEST() { GEN_TEST chanmon_deser GEN_TEST chanmon_consistency GEN_TEST full_stack +GEN_TEST offer_deser GEN_TEST onion_message GEN_TEST peer_crypt GEN_TEST process_network_graph diff --git a/fuzz/src/bin/offer_deser_target.rs b/fuzz/src/bin/offer_deser_target.rs new file mode 100644 index 00000000000..49563b10291 --- /dev/null +++ b/fuzz/src/bin/offer_deser_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::offer_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + offer_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + offer_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + offer_deser_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + offer_deser_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + offer_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/offer_deser") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + offer_deser_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 462307d55b4..dee4dbccb07 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -18,6 +18,7 @@ pub mod chanmon_deser; pub mod chanmon_consistency; pub mod full_stack; pub mod indexedmap; +pub mod offer_deser; pub mod onion_message; pub mod peer_crypt; pub mod process_network_graph; diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs new file mode 100644 index 00000000000..213742d8c08 --- /dev/null +++ b/fuzz/src/offer_deser.rs @@ -0,0 +1,69 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; +use lightning::offers::offer::{Amount, Offer, Quantity}; +use lightning::offers::parse::SemanticError; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(offer) = Offer::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + offer.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let pubkey = PublicKey::from(keys); + let mut buffer = Vec::new(); + + if let Ok(invoice_request) = build_response(&offer, pubkey) { + invoice_request + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } + } +} + +fn build_response<'a>( + offer: &'a Offer, pubkey: PublicKey +) -> Result, SemanticError> { + let mut builder = offer.request_invoice(vec![42; 64], pubkey)?; + + builder = match offer.amount() { + None => builder.amount_msats(1000).unwrap(), + Some(Amount::Bitcoin { amount_msats }) => builder.amount_msats(amount_msats + 1)?, + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), + }; + + builder = match offer.supported_quantity() { + Quantity::Bounded(n) => builder.quantity(n.get()).unwrap(), + Quantity::Unbounded => builder.quantity(10).unwrap(), + Quantity::One => builder, + }; + + builder.build() +} + +pub fn offer_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn offer_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/targets.h b/fuzz/targets.h index 5bfee07dafb..b09aacc4a87 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -2,6 +2,7 @@ void chanmon_deser_run(const unsigned char* data, size_t data_len); void chanmon_consistency_run(const unsigned char* data, size_t data_len); void full_stack_run(const unsigned char* data, size_t data_len); +void offer_deser_run(const unsigned char* data, size_t data_len); void onion_message_run(const unsigned char* data, size_t data_len); void peer_crypt_run(const unsigned char* data, size_t data_len); void process_network_graph_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a535b378250..b0819f9e920 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -242,7 +242,7 @@ impl OfferBuilder { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. @@ -254,7 +254,7 @@ pub struct Offer { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct OfferContents { chains: Option>, metadata: Option>, From fcb67434d9c1b74d955fb60cf9ade1d8e119bf02 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 23 Jan 2023 21:52:11 -0600 Subject: [PATCH 06/15] Make BlindedPayInfo fields public BlindedPayInfo fields need to be public in order to construct one for fuzz tests. Typically, they would be constructed from ChannelUpdateInfo for public channels and ChannelDetails for unannounced channels. For now, make the fields public so they can be constructed manually. --- lightning/src/offers/invoice.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 2d4b57d1a24..64d4c5d3dce 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -581,12 +581,30 @@ type BlindedPayInfoIter<'a> = core::iter::Map< /// Information needed to route a payment across a [`BlindedPath`]. #[derive(Clone, Debug, PartialEq)] pub struct BlindedPayInfo { - fee_base_msat: u32, - fee_proportional_millionths: u32, - cltv_expiry_delta: u16, - htlc_minimum_msat: u64, - htlc_maximum_msat: u64, - features: BlindedHopFeatures, + /// Base fee charged (in millisatoshi) for the entire blinded path. + pub fee_base_msat: u32, + + /// Liquidity fee charged (in millionths of the amount transferred) for the entire blinded path + /// (i.e., 10,000 is 1%). + pub fee_proportional_millionths: u32, + + /// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for the entire blinded + /// path. + pub cltv_expiry_delta: u16, + + /// The minimum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_minimum_msat: u64, + + /// The maximum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_maximum_msat: u64, + + /// Features set in `encrypted_data_tlv` for the `encrypted_recipient_data` TLV record in an + /// onion payload. + pub features: BlindedHopFeatures, } impl_writeable!(BlindedPayInfo, { From a320a4daf54c5b8f8b14121477b78b194defffbd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sun, 5 Feb 2023 21:32:38 -0600 Subject: [PATCH 07/15] Fix RoutingFees::base_msat docs --- lightning/src/routing/gossip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 363f067b1aa..d3e476fd5fc 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -971,7 +971,7 @@ impl EffectiveCapacity { /// Fees for routing via a given channel or a node #[derive(Eq, PartialEq, Copy, Clone, Debug, Hash)] pub struct RoutingFees { - /// Flat routing fee in satoshis + /// Flat routing fee in millisatoshis. pub base_msat: u32, /// Liquidity-based routing fee in millionths of a routed amount. /// In other words, 10000 is 1%. From 0001260f745687aefb721c853a3052fd18b98a21 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 20 Jan 2023 13:01:47 -0600 Subject: [PATCH 08/15] Fuzz test for parsing Refund A refund is serialized as a TLV stream and encoded in bech32 without a checksum. Add a fuzz test that parses the unencoded TLV stream and deserializes the underlying Refund. Then compare the original bytes with those obtained by re-serializing the Refund. --- fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/refund_deser_target.rs | 113 ++++++++++++++++++++++++++++ fuzz/src/lib.rs | 1 + fuzz/src/refund_deser.rs | 101 +++++++++++++++++++++++++ fuzz/targets.h | 1 + lightning/src/offers/payer.rs | 2 +- lightning/src/offers/refund.rs | 4 +- 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 fuzz/src/bin/refund_deser_target.rs create mode 100644 fuzz/src/refund_deser.rs diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index f322e60697a..946e845cb2f 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -13,6 +13,7 @@ GEN_TEST offer_deser GEN_TEST onion_message GEN_TEST peer_crypt GEN_TEST process_network_graph +GEN_TEST refund_deser GEN_TEST router GEN_TEST zbase32 GEN_TEST indexedmap diff --git a/fuzz/src/bin/refund_deser_target.rs b/fuzz/src/bin/refund_deser_target.rs new file mode 100644 index 00000000000..c9857783467 --- /dev/null +++ b/fuzz/src/bin/refund_deser_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::refund_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + refund_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + refund_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + refund_deser_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + refund_deser_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + refund_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/refund_deser") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + refund_deser_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index dee4dbccb07..05129056a4c 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -22,6 +22,7 @@ pub mod offer_deser; pub mod onion_message; pub mod peer_crypt; pub mod process_network_graph; +pub mod refund_deser; pub mod router; pub mod zbase32; diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs new file mode 100644 index 00000000000..9adaa3e953c --- /dev/null +++ b/fuzz/src/refund_deser.rs @@ -0,0 +1,101 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::chain::keysinterface::EntropySource; +use lightning::ln::PaymentHash; +use lightning::ln::features::BlindedHopFeatures; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedInvoice}; +use lightning::offers::parse::SemanticError; +use lightning::offers::refund::Refund; +use lightning::onion_message::BlindedPath; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(refund) = Refund::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + refund.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let pubkey = PublicKey::from(keys); + let mut buffer = Vec::new(); + + if let Ok(invoice) = build_response(&refund, pubkey, &secp_ctx) { + invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } + } +} + +struct Randomness; + +impl EntropySource for Randomness { + fn get_secure_random_bytes(&self) -> [u8; 32] { [42; 32] } +} + +fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + +fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( + refund: &'a Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 +) -> Result, SemanticError> { + let entropy_source = Randomness {}; + let paths = vec![ + BlindedPath::new(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + BlindedPath::new(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + let payment_paths = paths.into_iter().zip(payinfo.into_iter()).collect(); + let payment_hash = PaymentHash([42; 32]); + refund.respond_with(payment_paths, payment_hash, signing_pubkey)?.build() +} + +pub fn refund_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn refund_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/targets.h b/fuzz/targets.h index b09aacc4a87..e46b68af258 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -6,6 +6,7 @@ void offer_deser_run(const unsigned char* data, size_t data_len); void onion_message_run(const unsigned char* data, size_t data_len); void peer_crypt_run(const unsigned char* data, size_t data_len); void process_network_graph_run(const unsigned char* data, size_t data_len); +void refund_deser_run(const unsigned char* data, size_t data_len); void router_run(const unsigned char* data, size_t data_len); void zbase32_run(const unsigned char* data, size_t data_len); void indexedmap_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index e389a8f6d5d..7e1da769eda 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -17,7 +17,7 @@ use crate::prelude::*; /// [`InvoiceRequest::payer_id`]. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct PayerContents(pub Vec); tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 1488fbe97e1..cc0388c0241 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -216,7 +216,7 @@ impl RefundBuilder { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Refund { pub(super) bytes: Vec, pub(super) contents: RefundContents, @@ -225,7 +225,7 @@ pub struct Refund { /// The contents of a [`Refund`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct RefundContents { payer: PayerContents, // offer fields From 9a657092396de98aa220149a5fbfd8dbded8512a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 7 Feb 2023 18:29:08 -0600 Subject: [PATCH 09/15] Derive traits for InvoiceRequest and Invoice Offer and Refund derive Debug, Clone, and PartialEq. For consistency, derive these traits for InvoiceRequest and Invoice as well. --- lightning/src/offers/invoice.rs | 3 +++ lightning/src/offers/invoice_request.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 64d4c5d3dce..d1b1e99bad3 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -307,6 +307,7 @@ impl<'a> UnsignedInvoice<'a> { /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +#[derive(Clone, Debug, PartialEq)] pub struct Invoice { bytes: Vec, contents: InvoiceContents, @@ -317,6 +318,7 @@ pub struct Invoice { /// /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund +#[derive(Clone, Debug, PartialEq)] enum InvoiceContents { /// Contents for an [`Invoice`] corresponding to an [`Offer`]. /// @@ -335,6 +337,7 @@ enum InvoiceContents { } /// Invoice-specific fields for an `invoice` message. +#[derive(Clone, Debug, PartialEq)] struct InvoiceFields { payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 578b0a2398e..cc397f0f9a8 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -250,7 +250,7 @@ impl<'a> UnsignedInvoiceRequest<'a> { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct InvoiceRequest { pub(super) bytes: Vec, pub(super) contents: InvoiceRequestContents, @@ -260,7 +260,7 @@ pub struct InvoiceRequest { /// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct InvoiceRequestContents { payer: PayerContents, pub(super) offer: OfferContents, From e049e97993b292337e32864f47fa0a445d37e047 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 20 Jan 2023 13:34:34 -0600 Subject: [PATCH 10/15] Fuzz test for parsing InvoiceRequest An invoice request is serialized as a TLV stream and encoded as bytes. Add a fuzz test that parses the TLV stream and deserializes the underlying InvoiceRequest. Then compare the original bytes with those obtained by re-serializing the InvoiceRequest. --- fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/invoice_request_deser_target.rs | 113 +++++++++++++++++++ fuzz/src/invoice_request_deser.rs | 112 ++++++++++++++++++ fuzz/src/lib.rs | 1 + fuzz/targets.h | 1 + lightning/src/offers/invoice.rs | 9 +- 6 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 fuzz/src/bin/invoice_request_deser_target.rs create mode 100644 fuzz/src/invoice_request_deser.rs diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index 946e845cb2f..44d3ab29ea1 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -9,6 +9,7 @@ GEN_TEST() { GEN_TEST chanmon_deser GEN_TEST chanmon_consistency GEN_TEST full_stack +GEN_TEST invoice_request_deser GEN_TEST offer_deser GEN_TEST onion_message GEN_TEST peer_crypt diff --git a/fuzz/src/bin/invoice_request_deser_target.rs b/fuzz/src/bin/invoice_request_deser_target.rs new file mode 100644 index 00000000000..97741ff3ade --- /dev/null +++ b/fuzz/src/bin/invoice_request_deser_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::invoice_request_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + invoice_request_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + invoice_request_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + invoice_request_deser_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + invoice_request_deser_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + invoice_request_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/invoice_request_deser") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + invoice_request_deser_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs new file mode 100644 index 00000000000..aa3045ccb24 --- /dev/null +++ b/fuzz/src/invoice_request_deser.rs @@ -0,0 +1,112 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::secp256k1::{KeyPair, Parity, PublicKey, Secp256k1, SecretKey, self}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::chain::keysinterface::EntropySource; +use lightning::ln::PaymentHash; +use lightning::ln::features::BlindedHopFeatures; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedInvoice}; +use lightning::offers::invoice_request::InvoiceRequest; +use lightning::offers::parse::SemanticError; +use lightning::onion_message::BlindedPath; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(invoice_request) = InvoiceRequest::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + invoice_request.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut buffer = Vec::new(); + + if let Ok(unsigned_invoice) = build_response(&invoice_request, &secp_ctx) { + let signing_pubkey = unsigned_invoice.signing_pubkey(); + let (x_only_pubkey, _) = keys.x_only_public_key(); + let odd_pubkey = x_only_pubkey.public_key(Parity::Odd); + let even_pubkey = x_only_pubkey.public_key(Parity::Even); + if signing_pubkey == odd_pubkey || signing_pubkey == even_pubkey { + unsigned_invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } else { + unsigned_invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap_err(); + } + } + } +} + +struct Randomness; + +impl EntropySource for Randomness { + fn get_secure_random_bytes(&self) -> [u8; 32] { [42; 32] } +} + +fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + +fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( + invoice_request: &'a InvoiceRequest, secp_ctx: &Secp256k1 +) -> Result, SemanticError> { + let entropy_source = Randomness {}; + let paths = vec![ + BlindedPath::new(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + BlindedPath::new(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + let payment_paths = paths.into_iter().zip(payinfo.into_iter()).collect(); + let payment_hash = PaymentHash([42; 32]); + invoice_request.respond_with(payment_paths, payment_hash)?.build() +} + +pub fn invoice_request_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn invoice_request_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 05129056a4c..68b35d4ee2c 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -18,6 +18,7 @@ pub mod chanmon_deser; pub mod chanmon_consistency; pub mod full_stack; pub mod indexedmap; +pub mod invoice_request_deser; pub mod offer_deser; pub mod onion_message; pub mod peer_crypt; diff --git a/fuzz/targets.h b/fuzz/targets.h index e46b68af258..09187a59a3e 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -2,6 +2,7 @@ void chanmon_deser_run(const unsigned char* data, size_t data_len); void chanmon_consistency_run(const unsigned char* data, size_t data_len); void full_stack_run(const unsigned char* data, size_t data_len); +void invoice_request_deser_run(const unsigned char* data, size_t data_len); void offer_deser_run(const unsigned char* data, size_t data_len); void onion_message_run(const unsigned char* data, size_t data_len); void peer_crypt_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index d1b1e99bad3..7a3438b6410 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -267,6 +267,11 @@ pub struct UnsignedInvoice<'a> { } impl<'a> UnsignedInvoice<'a> { + /// The public key corresponding to the key needed to sign the invoice. + pub fn signing_pubkey(&self) -> PublicKey { + self.invoice.fields().signing_pubkey + } + /// Signs the invoice using the given function. pub fn sign(self, sign: F) -> Result> where @@ -453,12 +458,12 @@ impl Invoice { &self.contents.fields().features } - /// The public key used to sign invoices. + /// The public key corresponding to the key used to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { self.contents.fields().signing_pubkey } - /// Signature of the invoice using [`Invoice::signing_pubkey`]. + /// Signature of the invoice verified using [`Invoice::signing_pubkey`]. pub fn signature(&self) -> Signature { self.signature } From 16168d4c07c6f5f4c636b58cdec914f7e28126b4 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 20 Jan 2023 16:30:45 -0600 Subject: [PATCH 11/15] Fuzz test for parsing Invoice An invoice is serialized as a TLV stream and encoded as bytes. Add a fuzz test that parses the TLV stream and deserializes the underlying Invoice. Then compare the original bytes with those obtained by re-serializing the Invoice. --- fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/invoice_deser_target.rs | 113 +++++++++++++++++++++++++++ fuzz/src/invoice_deser.rs | 31 ++++++++ fuzz/src/lib.rs | 1 + fuzz/targets.h | 1 + 5 files changed, 147 insertions(+) create mode 100644 fuzz/src/bin/invoice_deser_target.rs create mode 100644 fuzz/src/invoice_deser.rs diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index 44d3ab29ea1..fe10e664700 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -9,6 +9,7 @@ GEN_TEST() { GEN_TEST chanmon_deser GEN_TEST chanmon_consistency GEN_TEST full_stack +GEN_TEST invoice_deser GEN_TEST invoice_request_deser GEN_TEST offer_deser GEN_TEST onion_message diff --git a/fuzz/src/bin/invoice_deser_target.rs b/fuzz/src/bin/invoice_deser_target.rs new file mode 100644 index 00000000000..06dbbe3078c --- /dev/null +++ b/fuzz/src/bin/invoice_deser_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::invoice_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + invoice_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + invoice_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + invoice_deser_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + invoice_deser_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + invoice_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/invoice_deser") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + invoice_deser_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/invoice_deser.rs b/fuzz/src/invoice_deser.rs new file mode 100644 index 00000000000..7b93fd38db1 --- /dev/null +++ b/fuzz/src/invoice_deser.rs @@ -0,0 +1,31 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::utils::test_logger; +use lightning::offers::invoice::Invoice; +use lightning::util::ser::Writeable; +use std::convert::TryFrom; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(invoice) = Invoice::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + invoice.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + } +} + +pub fn invoice_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn invoice_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 68b35d4ee2c..3964086376a 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -18,6 +18,7 @@ pub mod chanmon_deser; pub mod chanmon_consistency; pub mod full_stack; pub mod indexedmap; +pub mod invoice_deser; pub mod invoice_request_deser; pub mod offer_deser; pub mod onion_message; diff --git a/fuzz/targets.h b/fuzz/targets.h index 09187a59a3e..53831866e37 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -2,6 +2,7 @@ void chanmon_deser_run(const unsigned char* data, size_t data_len); void chanmon_consistency_run(const unsigned char* data, size_t data_len); void full_stack_run(const unsigned char* data, size_t data_len); +void invoice_deser_run(const unsigned char* data, size_t data_len); void invoice_request_deser_run(const unsigned char* data, size_t data_len); void offer_deser_run(const unsigned char* data, size_t data_len); void onion_message_run(const unsigned char* data, size_t data_len); From 56a01de61d0eab8c3d343b51e1a500098baa5aea Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 9 Feb 2023 10:59:11 -0600 Subject: [PATCH 12/15] Expose Bech32Encode trait for fuzzing In order to fuzz test Bech32Encode parsing independent of the underlying message deserialization, the trait needs to be exposed. Conditionally expose it only for fuzzing. --- lightning/src/offers/parse.rs | 104 +++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 35c1425acc1..6afd4d68fef 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -10,69 +10,83 @@ //! Parsing and formatting for bech32 message encoding. use bitcoin::bech32; -use bitcoin::bech32::{FromBase32, ToBase32}; use bitcoin::secp256k1; use core::convert::TryFrom; -use core::fmt; use crate::io; use crate::ln::msgs::DecodeError; use crate::util::ser::SeekReadable; use crate::prelude::*; -/// Indicates a message can be encoded using bech32. -pub(super) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { - /// Human readable part of the message's bech32 encoding. - const BECH32_HRP: &'static str; - - /// Parses a bech32-encoded message into a TLV stream. - fn from_bech32_str(s: &str) -> Result { - // Offer encoding may be split by '+' followed by optional whitespace. - let encoded = match s.split('+').skip(1).next() { - Some(_) => { - for chunk in s.split('+') { - let chunk = chunk.trim_start(); - if chunk.is_empty() || chunk.contains(char::is_whitespace) { - return Err(ParseError::InvalidContinuation); +#[cfg(not(fuzzing))] +pub(super) use sealed::Bech32Encode; + +#[cfg(fuzzing)] +pub use sealed::Bech32Encode; + +mod sealed { + use bitcoin::bech32; + use bitcoin::bech32::{FromBase32, ToBase32}; + use core::convert::TryFrom; + use core::fmt; + use super::ParseError; + + use crate::prelude::*; + + /// Indicates a message can be encoded using bech32. + pub trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { + /// Human readable part of the message's bech32 encoding. + const BECH32_HRP: &'static str; + + /// Parses a bech32-encoded message into a TLV stream. + fn from_bech32_str(s: &str) -> Result { + // Offer encoding may be split by '+' followed by optional whitespace. + let encoded = match s.split('+').skip(1).next() { + Some(_) => { + for chunk in s.split('+') { + let chunk = chunk.trim_start(); + if chunk.is_empty() || chunk.contains(char::is_whitespace) { + return Err(ParseError::InvalidContinuation); + } } - } - let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::(); - Bech32String::Owned(s) - }, - None => Bech32String::Borrowed(s), - }; + let s: String = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect(); + Bech32String::Owned(s) + }, + None => Bech32String::Borrowed(s), + }; - let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?; + let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?; - if hrp != Self::BECH32_HRP { - return Err(ParseError::InvalidBech32Hrp); - } + if hrp != Self::BECH32_HRP { + return Err(ParseError::InvalidBech32Hrp); + } - let data = Vec::::from_base32(&data)?; - Self::try_from(data) - } + let data = Vec::::from_base32(&data)?; + Self::try_from(data) + } - /// Formats the message using bech32-encoding. - fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) - .expect("HRP is invalid").unwrap(); + /// Formats the message using bech32-encoding. + fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) + .expect("HRP is invalid").unwrap(); - Ok(()) + Ok(()) + } } -} -// Used to avoid copying a bech32 string not containing the continuation character (+). -enum Bech32String<'a> { - Borrowed(&'a str), - Owned(String), -} + // Used to avoid copying a bech32 string not containing the continuation character (+). + enum Bech32String<'a> { + Borrowed(&'a str), + Owned(String), + } -impl<'a> AsRef for Bech32String<'a> { - fn as_ref(&self) -> &str { - match self { - Bech32String::Borrowed(s) => s, - Bech32String::Owned(s) => s, + impl<'a> AsRef for Bech32String<'a> { + fn as_ref(&self) -> &str { + match self { + Bech32String::Borrowed(s) => s, + Bech32String::Owned(s) => s, + } } } } From 3d41df025de4df512e8486c9e321fcda8d1a2420 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 9 Feb 2023 11:09:23 -0600 Subject: [PATCH 13/15] Fuzz test for bech32 decoding Fuzz testing bech32 decoding along with deserializing the underlying message can result in overly exhaustive searches. Instead, the message deserializations are now fuzzed separately. Add fuzzing for bech32 decoding. --- fuzz/src/bech32_parse.rs | 57 ++++++++++++++ fuzz/src/bin/bech32_parse_target.rs | 113 ++++++++++++++++++++++++++++ fuzz/src/bin/gen_target.sh | 1 + fuzz/src/lib.rs | 1 + fuzz/targets.h | 1 + 5 files changed, 173 insertions(+) create mode 100644 fuzz/src/bech32_parse.rs create mode 100644 fuzz/src/bin/bech32_parse_target.rs diff --git a/fuzz/src/bech32_parse.rs b/fuzz/src/bech32_parse.rs new file mode 100644 index 00000000000..f3dd5ac9d1e --- /dev/null +++ b/fuzz/src/bech32_parse.rs @@ -0,0 +1,57 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::utils::test_logger; +use core::convert::TryFrom; +use lightning::offers::parse::{Bech32Encode, ParseError}; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(bech32_encoded) = std::str::from_utf8(data) { + if let Ok(bytes) = Bytes::from_bech32_str(bech32_encoded) { + let bech32_encoded = bytes.to_string(); + assert_eq!(bytes, Bytes::from_bech32_str(&bech32_encoded).unwrap()); + } + } +} + +#[derive(Debug, PartialEq)] +struct Bytes(Vec); + +impl Bech32Encode for Bytes { + const BECH32_HRP: &'static str = "lno"; +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom> for Bytes { + type Error = ParseError; + fn try_from(data: Vec) -> Result { + Ok(Bytes(data)) + } +} + +impl core::fmt::Display for Bytes { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + +pub fn bech32_parse_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn bech32_parse_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/bin/bech32_parse_target.rs b/fuzz/src/bin/bech32_parse_target.rs new file mode 100644 index 00000000000..629112f9fef --- /dev/null +++ b/fuzz/src/bin/bech32_parse_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::bech32_parse::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + bech32_parse_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + bech32_parse_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + bech32_parse_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + bech32_parse_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + bech32_parse_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/bech32_parse") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + bech32_parse_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index fe10e664700..d7928188d8c 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -6,6 +6,7 @@ GEN_TEST() { echo "void $1_run(const unsigned char* data, size_t data_len);" >> ../../targets.h } +GEN_TEST bech32_parse GEN_TEST chanmon_deser GEN_TEST chanmon_consistency GEN_TEST full_stack diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 3964086376a..92142e56423 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -14,6 +14,7 @@ extern crate hex; pub mod utils; +pub mod bech32_parse; pub mod chanmon_deser; pub mod chanmon_consistency; pub mod full_stack; diff --git a/fuzz/targets.h b/fuzz/targets.h index 53831866e37..8f846c5e037 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -1,4 +1,5 @@ #include +void bech32_parse_run(const unsigned char* data, size_t data_len); void chanmon_deser_run(const unsigned char* data, size_t data_len); void chanmon_consistency_run(const unsigned char* data, size_t data_len); void full_stack_run(const unsigned char* data, size_t data_len); From 32ed69a2bd3971e16f099ca721fa43b633309c77 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 15 Feb 2023 16:43:41 -0600 Subject: [PATCH 14/15] Fix amount overflow in Offer parsing and building An overflow can occur when multiplying the offer amount by the requested quantity when checking if the given amount is enough. Return an error instead of overflowing. --- lightning/src/offers/invoice_request.rs | 29 +++++++++++++++++++++++++ lightning/src/offers/offer.rs | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index cc397f0f9a8..a1a0520c622 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -828,6 +828,18 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, SemanticError::MissingAmount), } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } } #[test] @@ -1123,6 +1135,23 @@ mod tests { assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnsupportedCurrency)); }, } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidAmount)), + } } #[test] diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index b0819f9e920..405e2e278d8 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -431,7 +431,8 @@ impl OfferContents { }; if !self.expects_quantity() || quantity.is_some() { - let expected_amount_msats = offer_amount_msats * quantity.unwrap_or(1); + let expected_amount_msats = offer_amount_msats.checked_mul(quantity.unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount)?; let amount_msats = amount_msats.unwrap_or(expected_amount_msats); if amount_msats < expected_amount_msats { From 9c2a3d090b618b98f2cd89c0193c4866331375ea Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 15 Feb 2023 20:17:18 -0600 Subject: [PATCH 15/15] Fix amount overflow in Invoice building An overflow can occur when multiplying the offer amount by the requested quantity when no amount is given in the request. Return an error instead of overflowing. --- lightning/src/offers/invoice.rs | 37 +++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 7a3438b6410..49c03a44347 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -148,7 +148,8 @@ impl<'a> InvoiceBuilder<'a> { Some(amount_msats) => amount_msats, None => match invoice_request.contents.offer.amount() { Some(Amount::Bitcoin { amount_msats }) => { - amount_msats * invoice_request.quantity().unwrap_or(1) + amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount)? }, Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), None => return Err(SemanticError::MissingAmount), @@ -787,7 +788,7 @@ mod tests { use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; - use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef}; + use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; @@ -1177,6 +1178,38 @@ mod tests { assert_eq!(tlv_stream.amount, Some(1001)); } + #[test] + fn builds_invoice_with_quantity_from_request() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(2).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.amount_msats(), 2000); + assert_eq!(tlv_stream.amount, Some(2000)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } + } + #[test] fn builds_invoice_with_fallback_address() { let script = Script::new();