Refund metadata and payer id derivation
authorJeffrey Czyz <jkczyz@gmail.com>
Mon, 6 Feb 2023 21:10:07 +0000 (15:10 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Apr 2023 02:31:07 +0000 (21:31 -0500)
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

index 4628c334fbefa54d27386a127732c624e2976643..b87febc7f6d53a430421618f954133f3395f9735 100644 (file)
 
 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<T>>,
 }
 
-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<ES: Deref>(
+               description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               secp_ctx: &'a Secp256k1<T>, amount_msats: u64
+       ) -> Result<Self, SemanticError> 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.