From 868fee7d2d98d1a5dd971a90b71b6016d3cfca29 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 12 Jul 2024 12:16:23 -0500 Subject: [PATCH] Add Bolt12Invoice::verify_using_payer_data Invoices are authenticated by checking the payer metadata in the corresponding invoice request or refund. For all invoices requests and for refunds using blinded paths, this will be the encrypted payment id and a 128-bit nonce. Allows checking the unencrypted payment id and nonce explicitly instead of the payer metadata. This will be used by an upcoming change that includes the payment id and nonce in the invoice request's reply path and the refund's blinded paths instead of completely in the payer metadata, which mitigates de-anonymization attacks. --- lightning/src/offers/invoice.rs | 55 +++++++++++++++---------- lightning/src/offers/invoice_request.rs | 8 ++-- lightning/src/offers/refund.rs | 8 ++-- lightning/src/offers/signer.rs | 23 +++++++++++ 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 69eafbdc5..5ea2c2e60 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -119,11 +119,12 @@ use crate::ln::msgs::DecodeError; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; +use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents}; -use crate::offers::signer; +use crate::offers::signer::{Metadata, self}; use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, Readable, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -770,12 +771,31 @@ impl Bolt12Invoice { self.tagged_hash.as_digest().as_ref().clone() } - /// Verifies that the invoice was for a request or refund created using the given key. Returns - /// the associated [`PaymentId`] to use when sending the payment. + /// Verifies that the invoice was for a request or refund created using the given key by + /// checking the payer metadata from the invoice request. + /// + /// Returns the associated [`PaymentId`] to use when sending the payment. pub fn verify( &self, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result { - self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx) + let metadata = match &self.contents { + InvoiceContents::ForOffer { invoice_request, .. } => &invoice_request.inner.payer.0, + InvoiceContents::ForRefund { refund, .. } => &refund.payer.0, + }; + self.contents.verify(TlvStream::new(&self.bytes), metadata, key, secp_ctx) + } + + /// Verifies that the invoice was for a request or refund created using the given key by + /// checking a payment id and nonce included with the [`BlindedPath`] for which the invoice was + /// sent through. + pub fn verify_using_payer_data( + &self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + let metadata = Metadata::payer_data(payment_id, nonce, key); + match self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, secp_ctx) { + Ok(extracted_payment_id) => payment_id == extracted_payment_id, + Err(()) => false, + } } pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { @@ -1006,35 +1026,28 @@ impl InvoiceContents { } fn verify( - &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + &self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey, + secp_ctx: &Secp256k1 ) -> Result { let offer_records = tlv_stream.clone().range(OFFER_TYPES); let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { match record.r#type { PAYER_METADATA_TYPE => false, // Should be outside range - INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(), + INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(), _ => true, } }); let tlv_stream = offer_records.chain(invreq_records); - let (metadata, payer_id, iv_bytes) = match self { - InvoiceContents::ForOffer { invoice_request, .. } => { - (invoice_request.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES) - }, - InvoiceContents::ForRefund { refund, .. } => { - (refund.metadata(), refund.payer_id(), REFUND_IV_BYTES) - }, + let payer_id = self.payer_id(); + let iv_bytes = match self { + InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES, + InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES, }; - signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) - } - - fn derives_keys(&self) -> bool { - match self { - InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(), - InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(), - } + signer::verify_payer_metadata( + metadata.as_ref(), key, iv_bytes, payer_id, tlv_stream, secp_ctx, + ) } fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index b8e47bac5..2415d5f46 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -636,7 +636,7 @@ pub(super) struct InvoiceRequestContents { #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] pub(super) struct InvoiceRequestContentsWithoutPayerId { - payer: PayerContents, + pub(super) payer: PayerContents, pub(super) offer: OfferContents, chain: Option, amount_msats: Option, @@ -953,10 +953,6 @@ impl InvoiceRequestContents { self.inner.metadata() } - pub(super) fn derives_keys(&self) -> bool { - self.inner.payer.0.derives_payer_keys() - } - pub(super) fn chain(&self) -> ChainHash { self.inner.chain() } @@ -1421,6 +1417,7 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let ( @@ -1494,6 +1491,7 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let ( diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index d5171b3a6..fbd4758d6 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -415,7 +415,7 @@ pub struct Refund { #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] pub(super) struct RefundContents { - payer: PayerContents, + pub(super) payer: PayerContents, // offer fields description: String, absolute_expiry: Option, @@ -727,10 +727,6 @@ impl RefundContents { self.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } - pub(super) fn derives_keys(&self) -> bool { - self.payer.0.derives_payer_keys() - } - pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes(), @@ -1049,6 +1045,7 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); let mut tlv_stream = refund.as_tlv_stream(); tlv_stream.2.amount = Some(2000); @@ -1113,6 +1110,7 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let mut tlv_stream = refund.as_tlv_stream(); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index ca78bc422..0f1428760 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -50,6 +50,11 @@ pub(super) enum Metadata { /// This variant should only be used at verification time, never when building. RecipientData(Nonce), + /// Metadata for deriving keys included as payer data in a blinded path. + /// + /// This variant should only be used at verification time, never when building. + PayerData([u8; PaymentId::LENGTH + Nonce::LENGTH]), + /// Metadata to be derived from message contents and given material. /// /// This variant should only be used at building time. @@ -62,6 +67,16 @@ pub(super) enum Metadata { } impl Metadata { + pub fn payer_data(payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey) -> Self { + let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + + let mut bytes = [0u8; PaymentId::LENGTH + Nonce::LENGTH]; + bytes[..PaymentId::LENGTH].copy_from_slice(encrypted_payment_id.as_slice()); + bytes[PaymentId::LENGTH..].copy_from_slice(nonce.as_slice()); + + Metadata::PayerData(bytes) + } + pub fn as_bytes(&self) -> Option<&Vec> { match self { Metadata::Bytes(bytes) => Some(bytes), @@ -73,6 +88,7 @@ impl Metadata { match self { Metadata::Bytes(_) => false, Metadata::RecipientData(_) => { debug_assert!(false); false }, + Metadata::PayerData(_) => { debug_assert!(false); false }, Metadata::Derived(_) => true, Metadata::DerivedSigningPubkey(_) => true, } @@ -87,6 +103,7 @@ impl Metadata { // Nonce::LENGTH had been set explicitly. Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH, Metadata::RecipientData(_) => false, + Metadata::PayerData(_) => true, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -101,6 +118,7 @@ impl Metadata { // been set explicitly. Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH, Metadata::RecipientData(_) => true, + Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -115,6 +133,7 @@ impl Metadata { match self { Metadata::Bytes(_) => self, Metadata::RecipientData(_) => { debug_assert!(false); self }, + Metadata::PayerData(_) => { debug_assert!(false); self }, Metadata::Derived(_) => self, Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material), } @@ -126,6 +145,7 @@ impl Metadata { match self { Metadata::Bytes(_) => (self, None), Metadata::RecipientData(_) => { debug_assert!(false); (self, None) }, + Metadata::PayerData(_) => { debug_assert!(false); (self, None) }, Metadata::Derived(mut metadata_material) => { tlv_stream.write(&mut metadata_material.hmac).unwrap(); (Metadata::Bytes(metadata_material.derive_metadata()), None) @@ -151,6 +171,7 @@ impl AsRef<[u8]> for Metadata { match self { Metadata::Bytes(bytes) => &bytes, Metadata::RecipientData(nonce) => &nonce.0, + Metadata::PayerData(bytes) => bytes.as_slice(), Metadata::Derived(_) => { debug_assert!(false); &[] }, Metadata::DerivedSigningPubkey(_) => { debug_assert!(false); &[] }, } @@ -162,6 +183,7 @@ impl fmt::Debug for Metadata { match self { Metadata::Bytes(bytes) => bytes.fmt(f), Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f), + Metadata::PayerData(bytes) => bytes.fmt(f), Metadata::Derived(_) => f.write_str("Derived"), Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"), } @@ -178,6 +200,7 @@ impl PartialEq for Metadata { false }, Metadata::RecipientData(_) => false, + Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => false, } -- 2.39.5