From bbdd8738a8fd505d57def682528011b81eae8f6d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 8 Aug 2024 19:11:27 -0500 Subject: [PATCH] Add parsing tests for unknown BOLT12 TLVs --- lightning/src/offers/invoice.rs | 82 ++++++++++++++++- lightning/src/offers/invoice_request.rs | 69 ++++++++++++++- lightning/src/offers/offer.rs | 41 ++++++++- lightning/src/offers/refund.rs | 41 ++++++++- lightning/src/offers/static_invoice.rs | 113 ++++++++++++++++++++++-- 5 files changed, 327 insertions(+), 19 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 648c0fba6..6cb37ab53 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1234,7 +1234,10 @@ impl TryFrom> for Bolt12Invoice { } } -tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { +/// Valid type range for invoice TLV records. +pub(super) const INVOICE_TYPES: core::ops::Range = 160..240; + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, INVOICE_TYPES, { (160, paths: (Vec, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)), (162, blindedpay: (Vec, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)), (164, created_at: (u64, HighZeroBytesDroppedBigSize)), @@ -1245,7 +1248,7 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (174, features: (Bolt12InvoiceFeatures, WithoutLength)), (176, node_id: PublicKey), // Only present in `StaticInvoice`s. - (238, message_paths: (Vec, WithoutLength)), + (236, message_paths: (Vec, WithoutLength)), }); pub(super) type BlindedPathIter<'a> = core::iter::Map< @@ -1437,7 +1440,7 @@ pub(super) fn check_invoice_signing_pubkey( #[cfg(test)] mod tests { - use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; + use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; use bitcoin::constants::ChainHash; @@ -2494,7 +2497,78 @@ mod tests { } #[test] - fn fails_parsing_invoice_with_extra_tlv_records() { + fn parses_invoice_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + unsigned_invoice.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + unsigned_invoice.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index c6c9da82a..e33ccb616 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1242,7 +1242,7 @@ impl Readable for InvoiceRequestFields { #[cfg(test)] mod tests { - use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; + use super::{INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::constants::ChainHash; use bitcoin::network::Network; @@ -2294,7 +2294,72 @@ mod tests { } #[test] - fn fails_parsing_invoice_request_with_extra_tlv_records() { + fn parses_invoice_request_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + + unsigned_invoice_request.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request.clone()) { + Ok(invoice_request) => assert_eq!(invoice_request.bytes, encoded_invoice_request), + Err(e) => panic!("error parsing invoice_request: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().unwrap(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + + unsigned_invoice_request.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_invoice_request_with_out_of_range_tlv_records() { let secp_ctx = Secp256k1::new(); let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let invoice_request = OfferBuilder::new(keys.public_key()) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 8501b8d46..546d42b88 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1173,7 +1173,7 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, Offer, OfferTlvStreamRef, Quantity}; + use super::{Amount, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { super::OfferBuilder, @@ -1860,12 +1860,47 @@ mod tests { } #[test] - fn fails_parsing_offer_with_extra_tlv_records() { + fn parses_offer_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = OFFER_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer.clone()) { + Ok(offer) => assert_eq!(offer.bytes, encoded_offer), + Err(e) => panic!("error parsing offer: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = OFFER_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_offer_with_out_of_range_tlv_records() { let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); - BigSize(80).write(&mut encoded_offer).unwrap(); + BigSize(OFFER_TYPES.end).write(&mut encoded_offer).unwrap(); BigSize(32).write(&mut encoded_offer).unwrap(); [42u8; 32].write(&mut encoded_offer).unwrap(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 482c3b688..860de534c 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -944,7 +944,7 @@ mod tests { use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; - use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; + use crate::offers::invoice_request::{INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; use crate::offers::offer::OfferTlvStreamRef; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; @@ -1522,7 +1522,44 @@ mod tests { } #[test] - fn fails_parsing_refund_with_extra_tlv_records() { + fn parses_refund_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund.clone()) { + Ok(refund) => assert_eq!(refund.bytes, encoded_refund), + Err(e) => panic!("error parsing refund: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_refund_with_out_of_range_tlv_records() { let secp_ctx = Secp256k1::new(); let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let refund = RefundBuilder::new(vec![1; 32], keys.public_key(), 1000).unwrap() diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index bf88bd944..dd3fb8b15 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -130,10 +130,9 @@ impl<'a> StaticInvoiceBuilder<'a> { Ok(Self { offer_bytes: &offer.bytes, invoice, keys }) } - /// Builds a signed [`StaticInvoice`] after checking for valid semantics. - pub fn build_and_sign( - self, secp_ctx: &Secp256k1, - ) -> Result { + /// Builds an [`UnsignedStaticInvoice`] after checking for valid semantics, returning it along with + /// the [`Keypair`] needed to sign it. + pub fn build(self) -> Result<(UnsignedStaticInvoice, Keypair), Bolt12SemanticError> { #[cfg(feature = "std")] { if self.invoice.is_offer_expired() { @@ -149,7 +148,14 @@ impl<'a> StaticInvoiceBuilder<'a> { } let Self { offer_bytes, invoice, keys } = self; - let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice); + Ok((UnsignedStaticInvoice::new(&offer_bytes, invoice), keys)) + } + + /// Builds a signed [`StaticInvoice`] after checking for valid semantics. + pub fn build_and_sign( + self, secp_ctx: &Secp256k1, + ) -> Result { + let (unsigned_invoice, keys) = self.build()?; let invoice = unsigned_invoice .sign(|message: &UnsignedStaticInvoice| { Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys)) @@ -606,14 +612,15 @@ mod tests { use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice::InvoiceTlvStreamRef; + use crate::offers::invoice::{InvoiceTlvStreamRef, INVOICE_TYPES}; use crate::offers::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::static_invoice::{ - StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG, + StaticInvoice, StaticInvoiceBuilder, UnsignedStaticInvoice, DEFAULT_RELATIVE_EXPIRY, + SIGNATURE_TAG, }; use crate::offers::test_utils::*; use crate::sign::KeyMaterial; @@ -1185,7 +1192,97 @@ mod tests { } #[test] - fn fails_parsing_invoice_with_extra_tlv_records() { + fn parses_invoice_with_unknown_tlv_records() { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + unsigned_invoice.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .unwrap(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + unsigned_invoice.tagged_hash = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = invoice(); let mut encoded_invoice = Vec::new(); invoice.write(&mut encoded_invoice).unwrap(); -- 2.39.5