From 022eadc4dbf0f60179674f936d604cade6c5dd9e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 30 Jan 2023 14:57:43 -0600 Subject: [PATCH] Stateless verification of Invoice for Offer 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 | 27 +++- lightning/src/offers/invoice_request.rs | 179 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 1 + lightning/src/offers/offer.rs | 6 +- lightning/src/offers/payer.rs | 8 +- lightning/src/offers/signer.rs | 2 +- 6 files changed, 209 insertions(+), 14 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 9d83ce89..f5a613ac 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -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( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> 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( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> 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(), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index f617383f..a7cdbfc0 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -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( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> 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 = 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); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index f6827467..3b05899a 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -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]>, } diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 617496d4..9f22e9af 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -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 = 1..80; +pub(super) const OFFER_TYPES: core::ops::Range = 1..80; /// TLV record type for [`Offer::metadata`]. const OFFER_METADATA_TYPE: u64 = 4; diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 7609c466..bfc02b5d 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -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, WithoutLength)), + (PAYER_METADATA_TYPE, metadata: (Vec, WithoutLength)), }); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index a8ea941e..f6141e59 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -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, 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>, secp_ctx: &Secp256k1 ) -> bool { -- 2.30.2