From 3855252239749fb4f6c81ae76b04b8123edff108 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 20 May 2026 20:04:06 +0000 Subject: [PATCH 1/2] Encrypt `payment_metadata` when we build the payment secret In 657ac8f58e51af74c610375cb65cdad6f7a18c6b we started committing to the `payment_metadata` in the `payment_secret`. We'd largely assumed that downstream code could simply encrypt the `payment_metadata` itself before passing it to `lightning` and decrypt before reading it from `lightning`. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its `payment_metadata` if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted `payment_metadata`. Instead, here, we encrypt and decrypt the `payment_metadata` internally in `lightning`. This allows us to reuse the IV that is used for `lightning`-generated `payment_hash`es as the IV for the encrypted `payment_metadata` as well. Sadly, we don't have any similar IV for user-provided `payment_hash`es. In that case, we simply accept the limitations and document that users must avoid encrypting multiple `payment_metadata`s for payments with the same `payment_hash`. This avoids padding the size of the `payment_metadata` and should generally not be a material concern - `payment_hash` reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point `payment_metadata` should hopefully be the same. --- fuzz/src/chanmon_consistency.rs | 2 +- .../tests/lsps2_integration_tests.rs | 2 +- lightning/src/crypto/utils.rs | 15 +- lightning/src/ln/bolt11_payment_tests.rs | 16 +- lightning/src/ln/channelmanager.rs | 58 +++-- lightning/src/ln/functional_test_utils.rs | 2 +- lightning/src/ln/functional_tests.rs | 29 +-- lightning/src/ln/inbound_payment.rs | 112 +++++++-- lightning/src/ln/invoice_utils.rs | 21 +- .../src/ln/max_payment_path_len_tests.rs | 53 ++--- lightning/src/ln/payment_tests.rs | 218 ++++++++++++++++-- 11 files changed, 403 insertions(+), 125 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 4ff0e4a4a03..72134abdad8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1369,7 +1369,7 @@ impl PaymentTracker { let mut payment_preimage = PaymentPreimage([0; 32]); payment_preimage.0[0..8].copy_from_slice(&self.payment_ctr.to_be_bytes()); let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); - let secret = dest + let (secret, _no_metadata) = dest .create_inbound_payment_for_hash(hash, None, 3600, None, None) .expect("create_inbound_payment_for_hash failed"); assert!(self.payment_preimages.insert(hash, payment_preimage).is_none()); diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 92e6b33ebb6..d361215822c 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -120,7 +120,7 @@ fn create_jit_invoice( ) -> Result { // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; - let (payment_hash, payment_secret) = node + let (payment_hash, payment_secret, _) = node .node .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None) .map_err(|e| { diff --git a/lightning/src/crypto/utils.rs b/lightning/src/crypto/utils.rs index 88911b0baf8..749f7d423c0 100644 --- a/lightning/src/crypto/utils.rs +++ b/lightning/src/crypto/utils.rs @@ -22,7 +22,7 @@ macro_rules! hkdf_extract_expand { let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm); (k1, k2) }}; - ($salt: expr, $ikm: expr, 7) => {{ + ($salt: expr, $ikm: expr, 8) => {{ let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm); let mut hmac = HmacEngine::::new(&prk[..]); @@ -50,7 +50,12 @@ macro_rules! hkdf_extract_expand { hmac.input(&[7; 1]); let k7 = Hmac::from_engine(hmac).to_byte_array(); - (k1, k2, k3, k4, k5, k6, k7) + let mut hmac = HmacEngine::::new(&prk[..]); + hmac.input(&k7); + hmac.input(&[8; 1]); + let k8 = Hmac::from_engine(hmac).to_byte_array(); + + (k1, k2, k3, k4, k5, k6, k7, k8) }}; } @@ -58,10 +63,10 @@ pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32] hkdf_extract_expand!(salt, ikm, 2) } -pub fn hkdf_extract_expand_7x( +pub fn hkdf_extract_expand_8x( salt: &[u8], ikm: &[u8], -) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) { - hkdf_extract_expand!(salt, ikm, 7) +) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) { + hkdf_extract_expand!(salt, ikm, 8) } #[inline] diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 733e26d0f1b..3e0ebbbefc2 100644 --- a/lightning/src/ln/bolt11_payment_tests.rs +++ b/lightning/src/ln/bolt11_payment_tests.rs @@ -30,8 +30,10 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap(); + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone())) + .unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) @@ -41,7 +43,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { .duration_since_epoch(timestamp) .min_final_cltv_expiry_delta(144) .amount_milli_satoshis(50_000) - .payment_metadata(payment_metadata.clone()) + .payment_metadata(encrypted_metadata.unwrap()) .build_raw() .unwrap(); let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap(); @@ -97,8 +99,10 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap(); + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone())) + .unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) @@ -107,7 +111,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { .payment_secret(payment_secret) .duration_since_epoch(timestamp) .min_final_cltv_expiry_delta(144) - .payment_metadata(payment_metadata.clone()) + .payment_metadata(encrypted_metadata.unwrap()) .build_raw() .unwrap(); let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 25c86b3f0cb..2adb0a1ca59 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8462,7 +8462,7 @@ impl< payment_data, payment_context, phantom_shared_secret, - onion_fields, + mut onion_fields, has_recipient_created_payment_secret, invoice_request_opt, trampoline_shared_secret, @@ -8603,7 +8603,7 @@ impl< let verify_res = inbound_payment::verify( payment_hash, &payment_data, - onion_fields.payment_metadata.as_deref(), + onion_fields.payment_metadata.as_mut(), self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger, @@ -14372,24 +14372,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } - let (payment_hash, payment_secret) = match payment_hash { + let (payment_hash, payment_secret, payment_metadata) = match payment_hash { Some(payment_hash) => { - let payment_secret = self + let (payment_secret, payment_metadata) = self .create_inbound_payment_for_hash( payment_hash, amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, - payment_metadata.as_deref(), + payment_metadata, ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; - (payment_hash, payment_secret) + (payment_hash, payment_secret, payment_metadata) }, None => { self .create_inbound_payment( amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, - payment_metadata.as_deref(), + payment_metadata, ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))? }, @@ -14516,8 +14516,7 @@ pub struct Bolt11InvoiceParameters { /// onion by the sender, available as [`RecipientOnionFields::payment_metadata`] via /// [`Event::PaymentClaimable::onion_fields`]. /// - /// Note that because it is exposed to the sender in the invoice you should consider encrypting - /// it. It is committed to, however, so cannot be modified by the sender. + /// The metadata itself is encrypted and HMAC'd before being stored in the BOLT 11 invoice. pub payment_metadata: Option>, } @@ -15023,6 +15022,7 @@ impl< |amount_msats, relative_expiry| { self.create_inbound_payment(Some(amount_msats), relative_expiry, None, None) .map_err(|()| Bolt12SemanticError::InvalidAmount) + .map(|(preimage, secret, _no_metadata)| (preimage, secret)) }, None, )?; @@ -15033,8 +15033,8 @@ impl< Ok(invoice) } - /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing - /// to pay us. + /// Gets a payment secret, payment hash, and encrypts the `payment_metadata` for use in an + /// invoice given to a third party wishing to pay us. /// /// This differs from [`create_inbound_payment_for_hash`] only in that it generates the /// [`PaymentHash`] and [`PaymentPreimage`] for you. @@ -15065,8 +15065,8 @@ impl< /// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash pub fn create_inbound_payment( &self, min_value_msat: Option, invoice_expiry_delta_secs: u32, - min_final_cltv_expiry_delta: Option, payment_metadata: Option<&[u8]>, - ) -> Result<(PaymentHash, PaymentSecret), ()> { + min_final_cltv_expiry_delta: Option, payment_metadata: Option>, + ) -> Result<(PaymentHash, PaymentSecret, Option>), ()> { inbound_payment::create( &self.inbound_payment_key, min_value_msat, @@ -15078,8 +15078,8 @@ impl< ) } - /// Gets a [`PaymentSecret`] for a given [`PaymentHash`], for which the payment preimage is - /// stored external to LDK. + /// Gets a [`PaymentSecret`] for a given [`PaymentHash`] (for which the payment preimage is + /// stored external to LDK) and encrypts the `payment_metadata`. /// /// A [`PaymentClaimable`] event will only be generated if the [`PaymentSecret`] matches a /// payment secret fetched via this method or [`create_inbound_payment`], and which is at least @@ -15115,41 +15115,34 @@ impl< /// Note that a malicious eavesdropper can intuit whether an inbound payment was created by /// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime. /// - /// # Note - /// - /// If you register an inbound payment with this method, then serialize the `ChannelManager`, then - /// deserialize it with a node running 0.0.103 and earlier, the payment will fail to be received. - /// /// Errors if `min_value_msat` is greater than total bitcoin supply. /// - /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable - /// on versions of LDK prior to 0.0.114. - /// /// [`create_inbound_payment`]: Self::create_inbound_payment /// [`PaymentClaimable`]: events::Event::PaymentClaimable pub fn create_inbound_payment_for_hash( &self, payment_hash: PaymentHash, min_value_msat: Option, invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option, - payment_metadata: Option<&[u8]>, - ) -> Result { + payment_metadata: Option>, + ) -> Result<(PaymentSecret, Option>), ()> { inbound_payment::create_from_hash( &self.inbound_payment_key, min_value_msat, payment_hash, invoice_expiry_delta_secs, + &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, min_final_cltv_expiry, payment_metadata, ) } - /// Gets an LDK-generated payment preimage from a payment hash, metadata and secret that were - /// previously returned from [`create_inbound_payment`]. + /// Gets an LDK-generated payment preimage from a payment hash and secret and decrypts the + /// metadata (if any) that were previously returned from [`create_inbound_payment`]. /// /// [`create_inbound_payment`]: Self::create_inbound_payment - pub fn get_payment_preimage( + pub fn get_payment_preimage_decrypt_metadata( &self, payment_hash: PaymentHash, payment_secret: PaymentSecret, - payment_metadata: Option<&[u8]>, + payment_metadata: Option<&mut [u8]>, ) -> Result { let expanded_key = &self.inbound_payment_key; inbound_payment::get_payment_preimage( @@ -17235,7 +17228,9 @@ impl< relative_expiry, None, None, - ).map_err(|_| Bolt12SemanticError::InvalidAmount) + ) + .map_err(|_| Bolt12SemanticError::InvalidAmount) + .map(|(preimage, secret, _no_metadata)| (preimage, secret)) }; let (result, context) = match invoice_request { @@ -22137,7 +22132,8 @@ pub mod bench { payment_preimage.0[0..8].copy_from_slice(&payment_count.to_le_bytes()); payment_count += 1; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); - let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap(); + let (payment_secret, _no_payment_metadata) = + $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap(); $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret, 10_000), PaymentId(payment_hash.0), diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index bbb184d2e48..ac6f137d5bb 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -2800,7 +2800,7 @@ pub fn get_payment_preimage_hash( let payment_preimage = PaymentPreimage([*payment_count; 32]); *payment_count += 1; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); - let payment_secret = recipient + let (payment_secret, _) = recipient .node .create_inbound_payment_for_hash( payment_hash, diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 7393f354010..37dd5187700 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -293,7 +293,7 @@ pub fn test_duplicate_htlc_different_direction_onchain() { let (payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 900_000); let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[0], payment_value_msats); - let node_a_payment_secret = nodes[0] + let (node_a_payment_secret, _) = nodes[0] .node .create_inbound_payment_for_hash(payment_hash, None, 7200, None, None) .unwrap(); @@ -4159,7 +4159,7 @@ pub fn test_duplicate_payment_hash_one_failure_one_success() { let (our_payment_preimage, dup_payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3]], 900_000); - let payment_secret = nodes[4] + let (payment_secret, _) = nodes[4] .node .create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None, None) .unwrap(); @@ -4428,13 +4428,13 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 2nd HTLC (not added - smaller than dust limit + HTLC tx fee): let path_5: &[&[_]] = &[&[&nodes[2], &nodes[3], &nodes[5]]]; - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_1, None, 7200, None, None).unwrap(); let route = route_to_5.clone(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_1, payment_secret); // 3rd HTLC (not added - smaller than dust limit + HTLC tx fee): - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_2, None, 7200, None, None).unwrap(); let route = route_to_5; send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_2, payment_secret); @@ -4447,12 +4447,12 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000); // 6th HTLC: - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_3, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route.clone(), path_5, 1000000, hash_3, payment_secret); // 7th HTLC: - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_4, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_4, payment_secret); @@ -4461,7 +4461,7 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 9th HTLC (not added - smaller than dust limit + HTLC tx fee): let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], dust_limit_msat); - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_5, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_5, payment_secret); @@ -4470,7 +4470,7 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 11th HTLC: let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000); - let payment_secret = + let (payment_secret, _) = nodes[5].node.create_inbound_payment_for_hash(hash_6, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_6, payment_secret); @@ -6064,7 +6064,7 @@ pub fn test_check_htlc_underpaying() { .unwrap(); let (_, our_payment_hash, _) = get_payment_preimage_hash(&nodes[0], None, None); - let our_payment_secret = nodes[1] + let (our_payment_secret, _) = nodes[1] .node .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None, None) .unwrap(); @@ -7233,7 +7233,7 @@ pub fn test_preimage_storage() { create_announced_chan_between_nodes(&nodes, 0, 1); { - let (payment_hash, payment_secret) = + let (payment_hash, payment_secret, _) = nodes[1].node.create_inbound_payment(Some(100_000), 7200, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); @@ -7278,7 +7278,7 @@ pub fn test_bad_secret_hash() { let random_hash = PaymentHash([42; 32]); let random_secret = PaymentSecret([43; 32]); - let (our_payment_hash, our_payment_secret) = + let (our_payment_hash, our_payment_secret, _) = nodes[1].node.create_inbound_payment(Some(100_000), 2, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); @@ -9496,13 +9496,16 @@ fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash get_payment_preimage_hash(&nodes[1], Some(recv_value), Some(min_cltv_expiry_delta)); (hash, payment_preimage, payment_secret) } else { - let (hash, payment_secret) = nodes[1] + let (hash, payment_secret, _) = nodes[1] .node .create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta), None) .unwrap(); ( hash, - nodes[1].node.get_payment_preimage(hash, payment_secret, None).unwrap(), + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(hash, payment_secret, None) + .unwrap(), payment_secret, ) }; diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index b81c111f7a1..40b04777427 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -15,7 +15,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; -use crate::crypto::utils::hkdf_extract_expand_7x; +use crate::crypto::utils::hkdf_extract_expand_8x; use crate::ln::msgs; use crate::ln::msgs::MAX_VALUE_MSAT; use crate::offers::nonce::Nonce as LocalNonce; @@ -60,6 +60,8 @@ pub struct ExpandedKey { /// that this is not used for blinded paths that are not expected to be shared across nodes /// participating in a "phantom node". pub(crate) phantom_node_blinded_path_key: [u8; 32], + /// The key used to encrypt payment metadata. + metadata_enc_key: [u8; 32], } impl ExpandedKey { @@ -75,7 +77,8 @@ impl ExpandedKey { offers_encryption_key, spontaneous_pmt_key, phantom_node_blinded_path_key, - ) = hkdf_extract_expand_7x(b"LDK Inbound Payment Key Expansion", &key_material); + metadata_enc_key, + ) = hkdf_extract_expand_8x(b"LDK Inbound Payment Key Expansion", &key_material); Self { info_key, ldk_pmt_hash_key, @@ -84,6 +87,7 @@ impl ExpandedKey { offers_encryption_key, spontaneous_pmt_key, phantom_node_blinded_path_key, + metadata_enc_key, } } @@ -150,13 +154,16 @@ fn min_final_cltv_expiry_delta_from_info(bytes: [u8; INFO_LEN]) -> u16 { /// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable /// on versions of LDK prior to 0.0.114. /// +/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of +/// validation. +/// /// [phantom node payments]: crate::sign::PhantomKeysManager /// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key pub fn create( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option, - payment_metadata: Option<&[u8]>, -) -> Result<(PaymentHash, PaymentSecret), ()> { + mut payment_metadata: Option>, +) -> Result<(PaymentHash, PaymentSecret, Option>), ()> { let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { @@ -173,10 +180,19 @@ pub fn create( let rand_bytes = entropy_source.get_secure_random_bytes(); iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); + if let Some(metadata) = payment_metadata.as_mut() { + ChaCha20::new_from_block( + Key::new(keys.metadata_enc_key), + Nonce::new(iv_bytes[4..].try_into().unwrap()), + u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), + ) + .apply_keystream(metadata.as_mut_slice()); + } + let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); hmac.input(&iv_bytes); hmac.input(&info_bytes); - if let Some(metadata) = payment_metadata { + if let Some(metadata) = payment_metadata.as_ref() { hmac.input(&(metadata.len() as u64).to_le_bytes()); hmac.input(metadata); } @@ -184,7 +200,7 @@ pub fn create( let ldk_pmt_hash = PaymentHash(Sha256::hash(&payment_preimage_bytes).to_byte_array()); let payment_secret = construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key); - Ok((ldk_pmt_hash, payment_secret)) + Ok((ldk_pmt_hash, payment_secret, payment_metadata)) } /// Equivalent to [`crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash`], @@ -196,12 +212,15 @@ pub fn create( /// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable /// on versions of LDK prior to 0.0.114. /// +/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of +/// validation. +/// /// [phantom node payments]: crate::sign::PhantomKeysManager -pub fn create_from_hash( +pub fn create_from_hash( keys: &ExpandedKey, min_value_msat: Option, payment_hash: PaymentHash, - invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option, - payment_metadata: Option<&[u8]>, -) -> Result { + invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, + min_final_cltv_expiry_delta: Option, mut payment_metadata: Option>, +) -> Result<(PaymentSecret, Option>), ()> { let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { @@ -214,10 +233,24 @@ pub fn create_from_hash( min_final_cltv_expiry_delta, )?; + if let Some(metadata) = payment_metadata.as_mut() { + let mut iv_bytes = [0 as u8; IV_LEN]; + let rand_bytes = entropy_source.get_secure_random_bytes(); + iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); + + ChaCha20::new_from_block( + Key::new(keys.metadata_enc_key), + Nonce::new(iv_bytes[4..16].try_into().unwrap()), + u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), + ) + .apply_keystream(metadata.as_mut_slice()); + metadata.extend_from_slice(&iv_bytes); + } + let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); hmac.input(&info_bytes); hmac.input(&payment_hash.0); - if let Some(metadata) = payment_metadata { + if let Some(metadata) = payment_metadata.as_ref() { hmac.input(&(metadata.len() as u64).to_le_bytes()); hmac.input(metadata); } @@ -226,7 +259,7 @@ pub fn create_from_hash( let mut iv_bytes = [0 as u8; IV_LEN]; iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]); - Ok(construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key)) + Ok((construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key), payment_metadata)) } pub(crate) fn create_for_spontaneous_payment( @@ -364,7 +397,8 @@ fn construct_payment_secret( /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash pub(super) fn verify( payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, - payment_metadata: Option<&[u8]>, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L, + mut payment_metadata: Option<&mut Vec>, highest_seen_timestamp: u64, keys: &ExpandedKey, + logger: &L, ) -> Result<(Option, Option), ()> { let (iv_bytes, info_bytes) = decrypt_info(payment_data.payment_secret, keys); @@ -385,7 +419,7 @@ pub(super) fn verify( let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); hmac.input(&info_bytes[..]); hmac.input(&payment_hash.0); - if let Some(metadata) = payment_metadata { + if let Some(metadata) = payment_metadata.as_deref() { hmac.input(&(metadata.len() as u64).to_le_bytes()); hmac.input(metadata); } @@ -399,6 +433,23 @@ pub(super) fn verify( &payment_hash ); return Err(()); + }; + + if let Some(metadata) = payment_metadata.as_mut() { + if metadata.len() < IV_LEN { + log_trace!(logger, "payment_metadata was shorter than expected IV. Failing HTLC with payment_hash {payment_hash}"); + return Err(()); + } + let new_len = metadata.len() - IV_LEN; + let (metadata_enc, metadata_iv) = metadata.split_at_mut(new_len); + + ChaCha20::new_from_block( + Key::new(keys.metadata_enc_key), + Nonce::new(metadata_iv[4..16].try_into().unwrap()), + u32::from_le_bytes(metadata_iv[..4].try_into().unwrap()), + ) + .apply_keystream(metadata_enc); + metadata.truncate(new_len); } }, Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { @@ -406,7 +457,7 @@ pub(super) fn verify( payment_hash, &iv_bytes, &info_bytes, - payment_metadata, + payment_metadata.as_deref().map(Vec::as_slice), keys, ) { Ok(preimage) => payment_preimage = Some(preimage), @@ -420,8 +471,21 @@ pub(super) fn verify( return Err(()); }, } + + if let Some(metadata) = payment_metadata { + ChaCha20::new_from_block( + Key::new(keys.metadata_enc_key), + Nonce::new(iv_bytes[4..].try_into().unwrap()), + u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), + ) + .apply_keystream(metadata); + } }, Ok(Method::SpontaneousPayment) => { + if payment_metadata.is_some() { + log_trace!(logger, "Shouldn't have a payment_metadata for a spontaneous payment, failing payment with hash {payment_hash}"); + return Err(()); + } let mut hmac = HmacEngine::::new(&keys.spontaneous_pmt_key); hmac.input(&info_bytes[..]); if !fixed_time_eq( @@ -470,18 +534,18 @@ pub(super) fn verify( } pub(super) fn get_payment_preimage( - payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&[u8]>, + payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&mut [u8]>, keys: &ExpandedKey, ) -> Result { let (iv_bytes, info_bytes) = decrypt_info(payment_secret, keys); match Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - derive_ldk_payment_preimage( + let preimage = derive_ldk_payment_preimage( payment_hash, &iv_bytes, &info_bytes, - payment_metadata, + payment_metadata.as_deref(), keys, ) .map_err(|bad_preimage_bytes| APIError::APIMisuseError { @@ -490,7 +554,17 @@ pub(super) fn get_payment_preimage( &payment_hash, log_bytes!(bad_preimage_bytes) ), - }) + })?; + + if let Some(metadata) = payment_metadata { + ChaCha20::new_from_block( + Key::new(keys.metadata_enc_key), + Nonce::new(iv_bytes[4..].try_into().unwrap()), + u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), + ) + .apply_keystream(metadata); + } + Ok(preimage) }, Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => { Err(APIError::APIMisuseError { diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 564203bf524..98996fa28bb 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -184,11 +184,12 @@ fn _create_phantom_invoice( let keys = node_signer.get_expanded_key(); let (payment_hash, payment_secret) = if let Some(payment_hash) = payment_hash { - let payment_secret = create_from_hash( + let (payment_secret, _no_metadata) = create_from_hash( &keys, amt_msat, payment_hash, invoice_expiry_delta_secs, + &entropy_source, duration_since_epoch.as_secs(), min_final_cltv_expiry_delta, None, @@ -196,7 +197,7 @@ fn _create_phantom_invoice( .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; (payment_hash, payment_secret) } else { - create( + let (payment_hash, payment_secret, _no_metadata) = create( &keys, amt_msat, invoice_expiry_delta_secs, @@ -205,7 +206,8 @@ fn _create_phantom_invoice( min_final_cltv_expiry_delta, None, ) - .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))? + .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; + (payment_hash, payment_secret) }; log_trace!( @@ -672,8 +674,10 @@ mod test { let (payment_hash, payment_secret) = (invoice.payment_hash(), *invoice.payment_secret()); - let preimage = - nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap(); + let preimage = nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(); // Invoice SCIDs should always use inbound SCID aliases over the real channel ID, if one is // available. @@ -1258,7 +1262,10 @@ mod test { let payment_preimage = if user_generated_pmt_hash { user_payment_preimage } else { - nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap() + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap() }; assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64); @@ -1365,7 +1372,7 @@ mod test { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001); let payment_amt = 20_000; - let (payment_hash, _payment_secret) = + let (payment_hash, _payment_secret, _) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None, None).unwrap(); let route_hints = vec![nodes[1].node.get_phantom_route_hints(), nodes[2].node.get_phantom_route_hints()]; diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 0bf73dbd8fb..c066f2c6d7b 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -32,7 +32,7 @@ use crate::routing::router::{ }; use crate::sign::NodeSigner; use crate::types::features::BlindedHopFeatures; -use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::types::payment::PaymentSecret; use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_utils; @@ -80,36 +80,33 @@ fn large_payment_metadata() { - final_payload_len_without_metadata; let mut payment_metadata = vec![42; max_metadata_len]; - let mut counter = 42; macro_rules! get_payment_hash { ($node: expr, $metadata: expr) => {{ - let payment_preimage = PaymentPreimage([counter; 32]); - #[allow(unused_assignments)] - { - counter += 1; - } - let payment_hash: PaymentHash = payment_preimage.into(); - let payment_secret = $node + let (payment_hash, payment_secret, encrypted_metadata) = $node .node - .create_inbound_payment_for_hash( + .create_inbound_payment(Some(amt_msat), 7200, None, Some($metadata)) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + let mut metadata_for_preimage = encrypted_metadata.clone(); + let payment_preimage = $node + .node + .get_payment_preimage_decrypt_metadata( payment_hash, - Some(amt_msat), - 7200, - None, - Some($metadata), + payment_secret, + Some(metadata_for_preimage.as_mut_slice()), ) .unwrap(); - (payment_hash, payment_preimage, payment_secret) + (payment_hash, payment_preimage, payment_secret, encrypted_metadata) }}; } // Check that the maximum-size metadata is sendable. - let (payment_hash, payment_preimage, payment_secret) = - get_payment_hash!(nodes[1], &payment_metadata); + let (payment_hash, payment_preimage, payment_secret, encrypted_metadata) = + get_payment_hash!(nodes[1], payment_metadata.clone()); let (mut route_0_1, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[1], amt_msat); let mut max_sized_onion = RecipientOnionFields { payment_secret: Some(payment_secret), - payment_metadata: Some(payment_metadata.clone()), + payment_metadata: Some(encrypted_metadata), custom_tlvs: Vec::new(), total_mpp_amount_msat: amt_msat, }; @@ -126,6 +123,7 @@ fn large_payment_metadata() { let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, events.pop().unwrap()) .with_payment_secret(payment_secret) + .with_payment_preimage(payment_preimage) .with_payment_metadata(payment_metadata.clone()); do_pass_along_path(args); claim_payment_along_route(ClaimAlongRouteArgs::new( @@ -137,13 +135,14 @@ fn large_payment_metadata() { // Check that the payment parameter for max path length will prevent us from routing past our // next-hop peer given the payment_metadata size. - let (payment_hash_2, _, payment_secret_2) = - get_payment_hash!(nodes[2], &max_sized_onion.payment_metadata.as_ref().unwrap()); + let (payment_hash_2, _, payment_secret_2, encrypted_metadata_2) = + get_payment_hash!(nodes[2], payment_metadata.clone()); let (mut route_0_2, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[2], amt_msat); let mut route_params_0_2 = route_0_2.route_params.clone().unwrap(); route_params_0_2.payment_params.max_path_length = 1; nodes[0].router.expect_find_route_query(route_params_0_2); max_sized_onion.payment_secret = Some(payment_secret_2); + max_sized_onion.payment_metadata = Some(encrypted_metadata_2); let id = PaymentId(payment_hash_2.0); let mut route_params = route_0_2.route_params.clone().unwrap(); @@ -155,10 +154,11 @@ fn large_payment_metadata() { // If our payment_metadata contains 1 additional byte, we'll fail prior to pathfinding. let mut too_large_onion = max_sized_onion.clone(); - too_large_onion.payment_metadata.as_mut().map(|mut md| md.push(42)); + too_large_onion.payment_metadata.as_mut().map(|md| md.push(42)); too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; - let (payment_hash_2, _, payment_secret_2) = - get_payment_hash!(nodes[2], &too_large_onion.payment_metadata.as_ref().unwrap()); + let mut too_large_metadata = payment_metadata.clone(); + too_large_metadata.push(42); + let (payment_hash_2, _, payment_secret_2, _) = get_payment_hash!(nodes[2], too_large_metadata); too_large_onion.payment_secret = Some(payment_secret_2); // First confirm we'll fail to create the onion packet directly. @@ -194,11 +194,11 @@ fn large_payment_metadata() { // If we remove enough payment_metadata bytes to allow for 2 hops, we're now able to send to // nodes[2]. let two_hop_metadata = vec![42; max_metadata_len - INTERMED_PAYLOAD_LEN_ESTIMATE]; - let (payment_hash_2, payment_preimage_2, payment_secret_2) = - get_payment_hash!(nodes[2], &two_hop_metadata); + let (payment_hash_2, payment_preimage_2, payment_secret_2, two_hop_encrypted_metadata) = + get_payment_hash!(nodes[2], two_hop_metadata.clone()); let mut onion_allowing_2_hops = RecipientOnionFields { payment_secret: Some(payment_secret_2), - payment_metadata: Some(two_hop_metadata.clone()), + payment_metadata: Some(two_hop_encrypted_metadata), custom_tlvs: Vec::new(), total_mpp_amount_msat: amt_msat, }; @@ -217,6 +217,7 @@ fn large_payment_metadata() { let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash_2, events.pop().unwrap()) .with_payment_secret(payment_secret_2) + .with_payment_preimage(payment_preimage_2) .with_payment_metadata(two_hop_metadata); do_pass_along_path(args); claim_payment_along_route(ClaimAlongRouteArgs::new( diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 2eb5d4ee85c..33c7df93ddb 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -25,8 +25,9 @@ use crate::ln::channel::{ EXPIRE_PREV_CONFIG_TICKS, }; use crate::ln::channelmanager::{ - HTLCForwardInfo, PaymentId, PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails, - BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS, + Bolt11InvoiceParameters, HTLCForwardInfo, OptionalBolt11PaymentParams, PaymentId, + PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails, BREAKDOWN_TIMEOUT, + MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS, }; use crate::ln::msgs; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; @@ -52,6 +53,8 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; + use crate::prelude::*; use crate::ln::functional_test_utils; @@ -1547,7 +1550,7 @@ fn get_ldk_payment_preimage() { let amt_msat = 60_000; let expiry_secs = 60 * 60; - let (payment_hash, payment_secret) = + let (payment_hash, payment_secret, _) = nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None, None).unwrap(); let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) @@ -1560,9 +1563,12 @@ fn get_ldk_payment_preimage() { nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); - // Make sure to use `get_payment_preimage` - let preimage = - Some(nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap()); + let preimage = Some( + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(), + ); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let event = events.pop().unwrap(); @@ -1572,6 +1578,182 @@ fn get_ldk_payment_preimage() { claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage.unwrap())); } +#[derive(Clone, Copy)] +enum PaymentMetadataSource { + Bolt11Invoice, + CreateInboundPayment, + CreateInboundPaymentForHash, +} + +fn do_payment_metadata_end_to_end(source: PaymentMetadataSource) { + // Generate a payment under each source, send a payment for it from another node, and verify + // that the `PaymentClaimable` event sees the (decrypted) payment_metadata that was originally + // provided. For sources which generate the preimage on our behalf, also check that + // `get_payment_preimage_decrypt_metadata` recovers the preimage and decrypts the metadata. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes(&nodes, 0, 1); + + let amt_msat = 50_000; + let node_b_id = nodes[1].node.get_our_node_id(); + let plaintext_metadata = vec![0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04, 0x05]; + + // Whenever LDK is computing the preimage itself (the `Bolt11Invoice` and + // `CreateInboundPayment` cases), `encrypted_metadata` holds the encrypted bytes so we can feed + // them back into `get_payment_preimage_decrypt_metadata` below. For the user-hash case we know + // the preimage up front so we stash it in `provided_preimage` instead. + let (payment_hash, payment_secret, encrypted_metadata, provided_preimage) = match source { + PaymentMetadataSource::Bolt11Invoice => { + let description = + Bolt11InvoiceDescription::Direct(Description::new("test".to_string()).unwrap()); + let invoice_params = Bolt11InvoiceParameters { + amount_msats: Some(amt_msat), + description, + payment_metadata: Some(plaintext_metadata.clone()), + ..Default::default() + }; + let invoice = nodes[1].node.create_bolt11_invoice(invoice_params).unwrap(); + let payment_hash = invoice.payment_hash(); + let payment_secret = *invoice.payment_secret(); + let encrypted_metadata = invoice.payment_metadata().unwrap().clone(); + // The encryption must produce different bytes than the plaintext for this test to be + // meaningful (otherwise the decryption could be a no-op and we wouldn't notice). + assert_ne!(encrypted_metadata, plaintext_metadata); + + nodes[0] + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(payment_hash.0), + None, + OptionalBolt11PaymentParams::default(), + ) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, None) + }, + PaymentMetadataSource::CreateInboundPayment => { + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment( + Some(amt_msat), + 7200, + None, + Some(plaintext_metadata.clone()), + ) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + assert_ne!(encrypted_metadata, plaintext_metadata); + + let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) + .unwrap(); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amt_msat); + let route = get_route(&nodes[0], &route_params).unwrap(); + let onion = RecipientOnionFields { + payment_secret: Some(payment_secret), + payment_metadata: Some(encrypted_metadata.clone()), + custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, + }; + nodes[0] + .node + .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0)) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, None) + }, + PaymentMetadataSource::CreateInboundPaymentForHash => { + let payment_preimage = PaymentPreimage([0x77; 32]); + let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); + let (payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment_for_hash( + payment_hash, + Some(amt_msat), + 7200, + None, + Some(plaintext_metadata.clone()), + ) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + assert_ne!(encrypted_metadata, plaintext_metadata); + + let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) + .unwrap(); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amt_msat); + let route = get_route(&nodes[0], &route_params).unwrap(); + let onion = RecipientOnionFields { + payment_secret: Some(payment_secret), + payment_metadata: Some(encrypted_metadata.clone()), + custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, + }; + nodes[0] + .node + .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0)) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, Some(payment_preimage)) + }, + }; + + check_added_monitors(&nodes[0], 1); + + // For sources where LDK derived the preimage, exercise + // `get_payment_preimage_decrypt_metadata`: it must recover the preimage *and* decrypt the + // metadata buffer in place. For the user-hash source we just use the preimage we picked. + let preimage = if let Some(preimage) = provided_preimage { + preimage + } else { + let mut decrypted_metadata = encrypted_metadata.clone(); + let preimage = nodes[1] + .node + .get_payment_preimage_decrypt_metadata( + payment_hash, + payment_secret, + Some(decrypted_metadata.as_mut_slice()), + ) + .unwrap(); + assert_eq!(decrypted_metadata, plaintext_metadata); + preimage + }; + assert_eq!(PaymentHash(Sha256::hash(&preimage.0).to_byte_array()), payment_hash); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = events.pop().unwrap(); + let path = &[&nodes[1]]; + let mut args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + .with_payment_secret(payment_secret) + .with_payment_metadata(plaintext_metadata.clone()); + // Only set the expected preimage when LDK is responsible for surfacing it on the receiver + // side (i.e. LDK-derived hashes). For user-supplied hashes, `PaymentClaimable` carries + // `payment_preimage: None`. + if provided_preimage.is_none() { + args = args.with_payment_preimage(preimage); + } + do_pass_along_path(args); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage)); +} + +#[test] +fn payment_metadata_end_to_end_bolt11_invoice() { + do_payment_metadata_end_to_end(PaymentMetadataSource::Bolt11Invoice); +} + +#[test] +fn payment_metadata_end_to_end_create_inbound_payment() { + do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPayment); +} + +#[test] +fn payment_metadata_end_to_end_create_inbound_payment_for_hash() { + do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPaymentForHash); +} + #[test] fn sent_probe_is_probe_of_sending_node() { let chanmon_cfgs = create_chanmon_cfgs(3); @@ -2305,7 +2487,7 @@ fn do_test_intercepted_payment(test: InterceptTest) { let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); let route = get_route(&nodes[0], &route_params).unwrap(); - let (hash, payment_secret) = + let (hash, payment_secret, _) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); @@ -2415,8 +2597,12 @@ fn do_test_intercepted_payment(test: InterceptTest) { do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, true); expect_and_process_pending_htlcs(&nodes[2], false); - let preimage = - Some(nodes[2].node.get_payment_preimage(hash, payment_secret, None).unwrap()); + let preimage = Some( + nodes[2] + .node + .get_payment_preimage_decrypt_metadata(hash, payment_secret, None) + .unwrap(), + ); expect_payment_claimable!(&nodes[2], hash, payment_secret, amt_msat, preimage, node_c_id); let path: &[&[_]] = &[&[&nodes[1], &nodes[2]]]; @@ -2542,7 +2728,7 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let (payment_hash, payment_secret) = + let (payment_hash, payment_secret, _) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); @@ -2598,8 +2784,10 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { } // Claim the payment and check that the skimmed fee is as expected. - let payment_preimage = - nodes[2].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap(); + let payment_preimage = nodes[2] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(); let events = nodes[2].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match events[0] { @@ -4890,14 +5078,14 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { let payment_metadata = vec![44, 49, 52, 142]; let payment_preimage = PaymentPreimage([42; 32]); let payment_hash: PaymentHash = payment_preimage.into(); - let payment_secret = nodes[3] + let (payment_secret, encrypted_metadata) = nodes[3] .node .create_inbound_payment_for_hash( payment_hash, Some(amt_msat), 7200, None, - Some(&payment_metadata), + Some(payment_metadata.clone()), ) .unwrap(); let payment_id = PaymentId(payment_hash.0); @@ -4910,7 +5098,7 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { // Send the MPP payment, delivering the updated commitment state to nodes[1]. let onion = RecipientOnionFields { payment_secret: Some(payment_secret), - payment_metadata: Some(payment_metadata), + payment_metadata: encrypted_metadata, custom_tlvs: vec![], total_mpp_amount_msat: amt_msat, }; From 4fac0fe1c174612b156a3adf664c8a9551a7b171 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 21 May 2026 18:57:26 +0000 Subject: [PATCH 2/2] Unify and simplify the application of simple chacha20 passes Most of our `chacha20` calls don't actually care about the concept of ChaCha20's "seek" vs "nonce" - we just want to use the full 128 bits of nonce space as nonce. Here we unify those calls to keep a consistent API and consolidate the `unwrap`s to one place. --- lightning/src/crypto/utils.rs | 11 ++++++ lightning/src/ln/inbound_payment.rs | 60 +++++------------------------ lightning/src/sign/mod.rs | 10 +---- 3 files changed, 23 insertions(+), 58 deletions(-) diff --git a/lightning/src/crypto/utils.rs b/lightning/src/crypto/utils.rs index 749f7d423c0..d6fa2044d79 100644 --- a/lightning/src/crypto/utils.rs +++ b/lightning/src/crypto/utils.rs @@ -3,6 +3,8 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; use bitcoin::secp256k1::{ecdsa::Signature, Message, Secp256k1, SecretKey, Signing}; +use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; + use crate::sign::EntropySource; macro_rules! hkdf_extract_expand { @@ -96,3 +98,12 @@ pub fn sign_with_aux_rand( let sig = sign(ctx, msg, sk); sig } + +pub fn apply_chacha20(key: [u8; 32], nonce: [u8; 16], data: &mut [u8]) { + ChaCha20::new_from_block( + Key::new(key), + Nonce::new(nonce[4..].try_into().unwrap()), + u32::from_le_bytes(nonce[..4].try_into().unwrap()), + ) + .apply_keystream(data); +} diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 40b04777427..077d2df60e5 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -13,9 +13,8 @@ use bitcoin::hashes::cmp::fixed_time_eq; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; -use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; -use crate::crypto::utils::hkdf_extract_expand_8x; +use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_8x}; use crate::ln::msgs; use crate::ln::msgs::MAX_VALUE_MSAT; use crate::offers::nonce::Nonce as LocalNonce; @@ -101,12 +100,7 @@ impl ExpandedKey { /// Encrypts or decrypts the given `bytes`. Used for data included in an offer message's /// metadata (e.g., payment id). pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], nonce: LocalNonce) -> [u8; 32] { - ChaCha20::new_from_block( - Key::new(self.offers_encryption_key), - Nonce::new(nonce.0[4..].try_into().unwrap()), - u32::from_le_bytes(nonce.0[..4].try_into().unwrap()), - ) - .apply_keystream(&mut bytes); + apply_chacha20(self.offers_encryption_key, nonce.0, &mut bytes); bytes } } @@ -181,12 +175,7 @@ pub fn create( iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); if let Some(metadata) = payment_metadata.as_mut() { - ChaCha20::new_from_block( - Key::new(keys.metadata_enc_key), - Nonce::new(iv_bytes[4..].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(metadata.as_mut_slice()); + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata.as_mut_slice()); } let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); @@ -238,12 +227,7 @@ pub fn create_from_hash( let rand_bytes = entropy_source.get_secure_random_bytes(); iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); - ChaCha20::new_from_block( - Key::new(keys.metadata_enc_key), - Nonce::new(iv_bytes[4..16].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(metadata.as_mut_slice()); + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata.as_mut_slice()); metadata.extend_from_slice(&iv_bytes); } @@ -349,12 +333,7 @@ fn construct_payment_secret( iv_slice.copy_from_slice(iv_bytes); encrypted_info_slice.copy_from_slice(info_bytes); - ChaCha20::new_from_block( - Key::new(*info_key), - Nonce::new(iv_bytes[4..].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(encrypted_info_slice); + apply_chacha20(*info_key, *iv_bytes, encrypted_info_slice); PaymentSecret(payment_secret_bytes) } @@ -442,13 +421,9 @@ pub(super) fn verify( } let new_len = metadata.len() - IV_LEN; let (metadata_enc, metadata_iv) = metadata.split_at_mut(new_len); + let metadata_iv: [u8; IV_LEN] = metadata_iv.try_into().expect("len checked"); - ChaCha20::new_from_block( - Key::new(keys.metadata_enc_key), - Nonce::new(metadata_iv[4..16].try_into().unwrap()), - u32::from_le_bytes(metadata_iv[..4].try_into().unwrap()), - ) - .apply_keystream(metadata_enc); + apply_chacha20(keys.metadata_enc_key, metadata_iv, metadata_enc); metadata.truncate(new_len); } }, @@ -473,12 +448,7 @@ pub(super) fn verify( } if let Some(metadata) = payment_metadata { - ChaCha20::new_from_block( - Key::new(keys.metadata_enc_key), - Nonce::new(iv_bytes[4..].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(metadata); + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata); } }, Ok(Method::SpontaneousPayment) => { @@ -557,12 +527,7 @@ pub(super) fn get_payment_preimage( })?; if let Some(metadata) = payment_metadata { - ChaCha20::new_from_block( - Key::new(keys.metadata_enc_key), - Nonce::new(iv_bytes[4..].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(metadata); + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata); } Ok(preimage) }, @@ -590,12 +555,7 @@ fn decrypt_info( let mut info_bytes: [u8; INFO_LEN] = [0; INFO_LEN]; info_bytes.copy_from_slice(encrypted_info_bytes); - ChaCha20::new_from_block( - Key::new(keys.info_key), - Nonce::new(iv_bytes[4..].try_into().unwrap()), - u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), - ) - .apply_keystream(&mut info_bytes); + apply_chacha20(keys.info_key, iv_bytes, &mut info_bytes); (iv_bytes, info_bytes) } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index a3dc72042cc..3adc6380297 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -34,12 +34,11 @@ use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::All; use bitcoin::secp256k1::{Keypair, PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{secp256k1, Psbt, Sequence, Txid, WPubkeyHash, Witness}; -use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; use lightning_invoice::RawBolt11Invoice; use crate::chain::transaction::OutPoint; -use crate::crypto::utils::{hkdf_extract_expand_twice, sign, sign_with_aux_rand}; +use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_countersigner_payment_script, get_revokeable_redeemscript, make_funding_redeemscript, @@ -2704,12 +2703,7 @@ impl EntropySource for RandomBytes { let mut nonce = [0u8; 16]; nonce[..8].copy_from_slice(&index.to_be_bytes()); let mut chacha_bytes = [0; 32]; - ChaCha20::new_from_block( - Key::new(self.seed), - Nonce::new(nonce[4..].try_into().unwrap()), - u32::from_le_bytes(nonce[..4].try_into().unwrap()), - ) - .apply_keystream(&mut chacha_bytes); + apply_chacha20(self.seed, nonce, &mut chacha_bytes); chacha_bytes } }