]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add Bolt12Invoice::verify_using_payer_data
authorJeffrey Czyz <jkczyz@gmail.com>
Fri, 12 Jul 2024 17:16:23 +0000 (12:16 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Mon, 22 Jul 2024 16:34:04 +0000 (11:34 -0500)
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
lightning/src/offers/invoice_request.rs
lightning/src/offers/refund.rs
lightning/src/offers/signer.rs

index 69eafbdc54901d0ffc17067d78eed90f77f46a5f..5ea2c2e6041545f0ca191219f26f2c647b32085b 100644 (file)
@@ -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<T: secp256k1::Signing>(
                &self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
        ) -> Result<PaymentId, ()> {
-               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<T: secp256k1::Signing>(
+               &self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> 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<T: secp256k1::Signing>(
-               &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+               &self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey,
+               secp_ctx: &Secp256k1<T>
        ) -> Result<PaymentId, ()> {
                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 {
index b8e47bac59e04e046052f606464f9e7d2d437575..2415d5f46b6e1c9f5defbb02409a359cc38d6a04 100644 (file)
@@ -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<ChainHash>,
        amount_msats: Option<u64>,
@@ -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 (
index d5171b3a692cde25282847b1e36b6f444d6896de..fbd4758d6a01c4d9aed4299d559fe89e27d2d5ae 100644 (file)
@@ -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<Duration>,
@@ -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();
index ca78bc422ebdebca3a001908f34103997066c513..0f14287608df6cfa2d7379f49365d36e34dc16e4 100644 (file)
@@ -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<u8>> {
                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,
                }