From: Jeffrey Czyz Date: Mon, 30 Jan 2023 20:57:43 +0000 (-0600) Subject: Stateless verification of Invoice for Offer X-Git-Tag: v0.0.115~12^2~6 X-Git-Url: http://git.bitcoin.ninja/?a=commitdiff_plain;h=022eadc4dbf0f60179674f936d604cade6c5dd9e;p=rust-lightning 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. --- diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 9d83ce899..f5a613aca 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 f617383fd..a7cdbfc0f 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 f68274674..3b05899a8 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 617496d4b..9f22e9af1 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 7609c4666..bfc02b5db 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 a8ea941e3..f6141e596 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 {