From 1a7540f2c936ec778e76050f22371829dbfa9255 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 6 Feb 2023 15:10:07 -0600 Subject: [PATCH] Refund metadata and payer id derivation Add support for deriving a transient payer id for each Refund from an ExpandedKey and a nonce. This facilitates payer privacy by not tying any Refund to any other nor to the payer's node id. Additionally, support stateless Invoice verification by setting payer metadata using an HMAC over the nonce and the remaining TLV records, which will be later verified when receiving an Invoice response. --- lightning/src/offers/refund.rs | 92 ++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 4628c334f..b87febc7f 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -73,20 +73,23 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{PublicKey, Secp256k1, self}; use core::convert::TryFrom; +use core::ops::Deref; use core::str::FromStr; use core::time::Duration; +use crate::chain::keysinterface::EntropySource; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::Metadata; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -96,16 +99,19 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~"; + /// Builds a [`Refund`] for the "offer for money" flow. /// /// See [module-level documentation] for usage. /// /// [module-level documentation]: self -pub struct RefundBuilder { +pub struct RefundBuilder<'a, T: secp256k1::Signing> { refund: RefundContents, + secp_ctx: Option<&'a Secp256k1>, } -impl RefundBuilder { +impl<'a> RefundBuilder<'a, secp256k1::SignOnly> { /// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to /// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey. /// @@ -119,13 +125,47 @@ impl RefundBuilder { } let metadata = Metadata::Bytes(metadata); - let refund = RefundContents { - payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), - quantity: None, payer_id, payer_note: None, - }; + Ok(Self { + refund: RefundContents { + payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, + paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id, payer_note: None, + }, + secp_ctx: None, + }) + } +} - Ok(RefundBuilder { refund }) +impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { + /// Similar to [`RefundBuilder::new`] except, if [`RefundBuilder::path`] is called, the payer id + /// is derived from the given [`ExpandedKey`] and nonce. This provides sender privacy by using a + /// different payer id for each refund, assuming a different nonce is used. Otherwise, the + /// provided `node_id` is used for the payer id. + /// + /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to + /// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey + pub fn deriving_payer_id( + description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + secp_ctx: &'a Secp256k1, amount_msats: u64 + ) -> Result where ES::Target: EntropySource { + if amount_msats > MAX_VALUE_MSAT { + return Err(SemanticError::InvalidAmount); + } + + let nonce = Nonce::from_entropy_source(entropy_source); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let metadata = Metadata::DerivedSigningPubkey(derivation_material); + Ok(Self { + refund: RefundContents { + payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, + paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id: node_id, payer_note: None, + }, + secp_ctx: Some(secp_ctx), + }) } /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has @@ -192,18 +232,38 @@ impl RefundBuilder { self.refund.chain = None; } + // Create the metadata for stateless verification of an Invoice. + if self.refund.payer.0.has_derivation_material() { + let mut metadata = core::mem::take(&mut self.refund.payer.0); + + if self.refund.paths.is_none() { + metadata = metadata.without_keys(); + } + + let mut tlv_stream = self.refund.as_tlv_stream(); + tlv_stream.0.metadata = None; + if metadata.derives_keys() { + tlv_stream.2.payer_id = None; + } + + let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx); + metadata = derived_metadata; + if let Some(keys) = keys { + self.refund.payer_id = keys.public_key(); + } + + self.refund.payer.0 = metadata; + } + let mut bytes = Vec::new(); self.refund.write(&mut bytes).unwrap(); - Ok(Refund { - bytes, - contents: self.refund, - }) + Ok(Refund { bytes, contents: self.refund }) } } #[cfg(test)] -impl RefundBuilder { +impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { self.refund.features = features; self @@ -283,7 +343,7 @@ impl Refund { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0.as_bytes().unwrap()[..] + self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) } /// A chain that the refund is valid for. -- 2.39.5