Stateless verification of Invoice for Offer
authorJeffrey Czyz <jkczyz@gmail.com>
Mon, 30 Jan 2023 20:57:43 +0000 (14:57 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Apr 2023 02:31:07 +0000 (21:31 -0500)
Verify that an Invoice was produced from an InvoiceRequest constructed
by the payer using the payer metadata reflected in the Invoice. The
payer metadata consists of a 128-bit encrypted nonce and possibly a
256-bit HMAC over the nonce and InvoiceRequest TLV records (excluding
the payer id) using an ExpandedKey.

Thus, the HMAC can be reproduced from the invoice request bytes using
the nonce and the original ExpandedKey, and then checked against the
metadata. If metadata does not contain an HMAC, then the reproduced HMAC
was used to form the signing keys, and thus can be checked against the
payer id.

lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/merkle.rs
lightning/src/offers/offer.rs
lightning/src/offers/payer.rs
lightning/src/offers/signer.rs

index 9d83ce899b153d836288facf83b82e7515b1dc90..f5a613aca6510ac5418f8f5c685c7256e2ef7d23 100644 (file)
@@ -97,7 +97,7 @@ use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
 use bitcoin::hashes::Hash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::{Message, PublicKey};
+use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
 use bitcoin::secp256k1::schnorr::Signature;
 use bitcoin::util::address::{Address, Payload, WitnessVersion};
 use bitcoin::util::schnorr::TweakedPublicKey;
@@ -106,9 +106,10 @@ use core::time::Duration;
 use crate::io;
 use crate::ln::PaymentHash;
 use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
+use crate::ln::inbound_payment::ExpandedKey;
 use crate::ln::msgs::DecodeError;
 use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
-use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self};
+use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self};
 use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
 use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
@@ -123,7 +124,7 @@ use std::time::SystemTime;
 
 const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
 
-const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
+pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
 
 /// Builds an [`Invoice`] from either:
 /// - an [`InvoiceRequest`] for the "offer to be paid" flow or
@@ -476,8 +477,15 @@ impl Invoice {
                merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
        }
 
+       /// Verifies that the invoice was for a request or refund created using the given key.
+       pub fn verify<T: secp256k1::Signing>(
+               &self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> bool {
+               self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
+       }
+
        #[cfg(test)]
-       fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
+       pub(super) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
                let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
                        self.contents.as_tlv_stream();
                let signature_tlv_stream = SignatureTlvStreamRef {
@@ -520,6 +528,17 @@ impl InvoiceContents {
                }
        }
 
+       fn verify<T: secp256k1::Signing>(
+               &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> bool {
+               match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => {
+                               invoice_request.verify(tlv_stream, key, secp_ctx)
+                       },
+                       _ => todo!(),
+               }
+       }
+
        fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
                let (payer, offer, invoice_request) = match self {
                        InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
index f617383fdcfa364c38bbc521b91a43554d6a35f4..a7cdbfc0f155157cfa7b7f8e1976953ee750d080 100644 (file)
@@ -66,10 +66,10 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
 use crate::ln::msgs::DecodeError;
 use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
 use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
-use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
+use crate::offers::offer::{OFFER_TYPES, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
-use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
-use crate::offers::signer::{Metadata, MetadataMaterial};
+use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
+use crate::offers::signer::{Metadata, MetadataMaterial, self};
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
@@ -532,6 +532,22 @@ impl InvoiceRequestContents {
                self.inner.chain()
        }
 
+       /// Verifies that the payer metadata was produced from the invoice request in the TLV stream.
+       pub(super) fn verify<T: secp256k1::Signing>(
+               &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> bool {
+               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.inner.payer.0.derives_keys(),
+                               _ => true,
+                       }
+               });
+               let tlv_stream = offer_records.chain(invreq_records);
+               signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx)
+       }
+
        pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
                let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream();
                invoice_request.payer_id = Some(&self.payer_id);
@@ -585,12 +601,20 @@ impl Writeable for InvoiceRequestContents {
        }
 }
 
-tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
+/// Valid type range for invoice_request TLV records.
+const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
+
+/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
+///
+/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
+const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
+
+tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
        (80, chain: ChainHash),
        (82, amount: (u64, HighZeroBytesDroppedBigSize)),
        (84, features: (InvoiceRequestFeatures, WithoutLength)),
        (86, quantity: (u64, HighZeroBytesDroppedBigSize)),
-       (88, payer_id: PublicKey),
+       (INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey),
        (89, payer_note: (String, WithoutLength)),
 });
 
@@ -702,8 +726,11 @@ mod tests {
        use core::num::NonZeroU64;
        #[cfg(feature = "std")]
        use core::time::Duration;
+       use crate::chain::keysinterface::KeyMaterial;
        use crate::ln::features::InvoiceRequestFeatures;
+       use crate::ln::inbound_payment::ExpandedKey;
        use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+       use crate::offers::invoice::{Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG};
        use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
        use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
        use crate::offers::parse::{ParseError, SemanticError};
@@ -800,6 +827,148 @@ mod tests {
                }
        }
 
+       #[test]
+       fn builds_invoice_request_with_derived_metadata() {
+               let payer_id = payer_pubkey();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .amount_msats(1000)
+                       .build().unwrap();
+               let invoice_request = offer
+                       .request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert_eq!(invoice_request.payer_id(), payer_pubkey());
+
+               let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered fields
+               let (
+                       payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
+                       mut invoice_tlv_stream, mut signature_tlv_stream
+               ) = invoice.as_tlv_stream();
+               invoice_request_tlv_stream.amount = Some(2000);
+               invoice_tlv_stream.amount = Some(2000);
+
+               let tlv_stream =
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
+               let mut bytes = Vec::new();
+               tlv_stream.write(&mut bytes).unwrap();
+
+               let signature = merkle::sign_message(
+                       recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
+               ).unwrap();
+               signature_tlv_stream.signature = Some(&signature);
+
+               let mut encoded_invoice = bytes;
+               signature_tlv_stream.write(&mut encoded_invoice).unwrap();
+
+               let invoice = Invoice::try_from(encoded_invoice).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered metadata
+               let (
+                       mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
+                       mut signature_tlv_stream
+               ) = invoice.as_tlv_stream();
+               let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect();
+               payer_tlv_stream.metadata = Some(&metadata);
+
+               let tlv_stream =
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
+               let mut bytes = Vec::new();
+               tlv_stream.write(&mut bytes).unwrap();
+
+               let signature = merkle::sign_message(
+                       recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
+               ).unwrap();
+               signature_tlv_stream.signature = Some(&signature);
+
+               let mut encoded_invoice = bytes;
+               signature_tlv_stream.write(&mut encoded_invoice).unwrap();
+
+               let invoice = Invoice::try_from(encoded_invoice).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+       }
+
+       #[test]
+       fn builds_invoice_request_with_derived_payer_id() {
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .amount_msats(1000)
+                       .build().unwrap();
+               let invoice_request = offer
+                       .request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
+                       .unwrap()
+                       .build_and_sign()
+                       .unwrap();
+
+               let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered fields
+               let (
+                       payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
+                       mut invoice_tlv_stream, mut signature_tlv_stream
+               ) = invoice.as_tlv_stream();
+               invoice_request_tlv_stream.amount = Some(2000);
+               invoice_tlv_stream.amount = Some(2000);
+
+               let tlv_stream =
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
+               let mut bytes = Vec::new();
+               tlv_stream.write(&mut bytes).unwrap();
+
+               let signature = merkle::sign_message(
+                       recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
+               ).unwrap();
+               signature_tlv_stream.signature = Some(&signature);
+
+               let mut encoded_invoice = bytes;
+               signature_tlv_stream.write(&mut encoded_invoice).unwrap();
+
+               let invoice = Invoice::try_from(encoded_invoice).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered payer id
+               let (
+                       payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream,
+                       mut signature_tlv_stream
+               ) = invoice.as_tlv_stream();
+               let payer_id = pubkey(1);
+               invoice_request_tlv_stream.payer_id = Some(&payer_id);
+
+               let tlv_stream =
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
+               let mut bytes = Vec::new();
+               tlv_stream.write(&mut bytes).unwrap();
+
+               let signature = merkle::sign_message(
+                       recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
+               ).unwrap();
+               signature_tlv_stream.signature = Some(&signature);
+
+               let mut encoded_invoice = bytes;
+               signature_tlv_stream.write(&mut encoded_invoice).unwrap();
+
+               let invoice = Invoice::try_from(encoded_invoice).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+       }
+
        #[test]
        fn builds_invoice_request_with_chain() {
                let mainnet = ChainHash::using_genesis_block(Network::Bitcoin);
index f682746742050749463c181340fdad72f3d2d554..3b05899a8f59214872a1075179fc9428b48412fa 100644 (file)
@@ -143,6 +143,7 @@ fn tagged_branch_hash_from_engine(
 
 /// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a
 /// well-formed TLV stream.
+#[derive(Clone)]
 pub(super) struct TlvStream<'a> {
        data: io::Cursor<&'a [u8]>,
 }
index 617496d4b5174f45e51b8c72b54309c96038fed6..9f22e9af184a887ae9654de48f58e4db4e7d04c1 100644 (file)
@@ -443,8 +443,8 @@ impl Offer {
        /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
        ///   request, and
        /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
-       ///   that it can be used to determine if the invoice was requested using a base [`ExpandedKey`]
-       ///   from which the payer id was derived.
+       ///   that it can be used by [`Invoice::verify`] to determine if the invoice was requested using
+       ///   a base [`ExpandedKey`] from which the payer id was derived.
        ///
        /// Useful to protect the sender's privacy.
        ///
@@ -722,7 +722,7 @@ impl Quantity {
 }
 
 /// Valid type range for offer TLV records.
-const OFFER_TYPES: core::ops::Range<u64> = 1..80;
+pub(super) const OFFER_TYPES: core::ops::Range<u64> = 1..80;
 
 /// TLV record type for [`Offer::metadata`].
 const OFFER_METADATA_TYPE: u64 = 4;
index 7609c4666197ce3c91f685811b12ca0bc34f718a..bfc02b5dbcb9804f1fd08bdf689f541a12579ed9 100644 (file)
@@ -22,6 +22,12 @@ use crate::prelude::*;
 #[cfg_attr(test, derive(PartialEq))]
 pub(super) struct PayerContents(pub Metadata);
 
+/// TLV record type for [`InvoiceRequest::metadata`] and [`Refund::metadata`].
+///
+/// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata
+/// [`Refund::metadata`]: crate::offers::refund::Refund::metadata
+pub(super) const PAYER_METADATA_TYPE: u64 = 0;
+
 tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
-       (0, metadata: (Vec<u8>, WithoutLength)),
+       (PAYER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
 });
index a8ea941e3be834e3504af6c23643db5219e11e33..f6141e59699addb391ff6bfb523f75767e81aa7f 100644 (file)
@@ -169,7 +169,7 @@ impl MetadataMaterial {
 /// If the latter is not included in the metadata, the TLV stream is used to check if the given
 /// `signing_pubkey` can be derived from it.
 pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
-       metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
+       metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
        signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
        secp_ctx: &Secp256k1<T>
 ) -> bool {