From 7a3e06b1e791e752dbe2cbd4747747c1611ad6f7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 21 Jul 2023 15:28:36 -0500 Subject: [PATCH] Include PaymentId in payer metadata When receiving a BOLT 12 invoice originating from either an invoice request or a refund, the invoice should only be paid once. To accomplish this, require that the invoice includes an encrypted payment id in the payer metadata. This allows ChannelManager to track a payment when requesting but prior to receiving the invoice. Thus, it can determine if the invoice has already been paid. --- lightning/src/ln/channelmanager.rs | 7 +- lightning/src/ln/inbound_payment.rs | 7 ++ lightning/src/offers/invoice.rs | 13 ++- lightning/src/offers/invoice_request.rs | 42 ++++++--- lightning/src/offers/offer.rs | 35 +++++--- lightning/src/offers/refund.rs | 38 +++++--- lightning/src/offers/signer.rs | 114 ++++++++++++++++++++++-- 7 files changed, 202 insertions(+), 54 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6393117b7..cf280fabc 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -237,7 +237,12 @@ impl From<&ClaimableHTLC> for events::ClaimedHTLC { /// /// This is not exported to bindings users as we just use [u8; 32] directly #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] -pub struct PaymentId(pub [u8; 32]); +pub struct PaymentId(pub [u8; Self::LENGTH]); + +impl PaymentId { + /// Number of bytes in the id. + pub const LENGTH: usize = 32; +} impl Writeable for PaymentId { fn write(&self, w: &mut W) -> Result<(), io::Error> { diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 25e79e3bc..f9e10880a 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -86,6 +86,13 @@ impl ExpandedKey { hmac.input(&nonce.0); hmac } + + /// 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: Nonce) -> [u8; 32] { + ChaCha20::encrypt_single_block_in_place(&self.offers_encryption_key, &nonce.0, &mut bytes); + bytes + } } /// A 128-bit number used only once. diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ed858fa6c..06215e2d4 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -110,6 +110,7 @@ use core::time::Duration; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::PaymentHash; +use crate::ln::channelmanager::PaymentId; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; @@ -695,10 +696,11 @@ impl Bolt12Invoice { 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. + /// Verifies that the invoice was for a request or refund created using the given key. Returns + /// the associated [`PaymentId`] to use when sending the payment. pub fn verify( &self, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { + ) -> Result { self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx) } @@ -947,7 +949,7 @@ impl InvoiceContents { fn verify( &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { + ) -> Result { let offer_records = tlv_stream.clone().range(OFFER_TYPES); let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { match record.r#type { @@ -967,10 +969,7 @@ impl InvoiceContents { }, }; - match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) { - Ok(_) => true, - Err(()) => false, - } + signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) } fn derives_keys(&self) -> bool { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 55cd6266f..fb0b0205b 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -64,6 +64,7 @@ use crate::sign::EntropySource; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::PaymentHash; +use crate::ln::channelmanager::PaymentId; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; @@ -128,10 +129,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI } pub(super) fn deriving_metadata( - offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES + offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + payment_id: PaymentId, ) -> Self where ES::Target: EntropySource { let nonce = Nonce::from_entropy_source(entropy_source); - let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let payment_id = Some(payment_id); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::Derived(derivation_material); Self { offer, @@ -145,10 +148,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> { pub(super) fn deriving_payer_id( - offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1 + offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, + secp_ctx: &'b Secp256k1, payment_id: PaymentId ) -> Self where ES::Target: EntropySource { let nonce = Nonce::from_entropy_source(entropy_source); - let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let payment_id = Some(payment_id); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::DerivedSigningPubkey(derivation_material); Self { offer, @@ -259,7 +264,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a let mut tlv_stream = self.invoice_request.as_tlv_stream(); debug_assert!(tlv_stream.2.payer_id.is_none()); tlv_stream.0.metadata = None; - if !metadata.derives_keys() { + if !metadata.derives_payer_keys() { tlv_stream.2.payer_id = self.payer_id.as_ref(); } @@ -691,7 +696,7 @@ impl InvoiceRequestContents { } pub(super) fn derives_keys(&self) -> bool { - self.inner.payer.0.derives_keys() + self.inner.payer.0.derives_payer_keys() } pub(super) fn chain(&self) -> ChainHash { @@ -924,6 +929,7 @@ mod tests { #[cfg(feature = "std")] use core::time::Duration; use crate::sign::KeyMaterial; + use crate::ln::channelmanager::PaymentId; use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; @@ -1069,12 +1075,13 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); 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) + .request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id) .unwrap() .build().unwrap() .sign(payer_sign).unwrap(); @@ -1084,7 +1091,10 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx)); + match invoice.verify(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let ( @@ -1107,7 +1117,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let ( @@ -1130,7 +1140,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -1138,12 +1148,13 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); 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) + .request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id) .unwrap() .build_and_sign() .unwrap(); @@ -1152,7 +1163,10 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx)); + match invoice.verify(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let ( @@ -1175,7 +1189,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer id let ( @@ -1198,7 +1212,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index f6aa354b9..e0bc63e8b 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -77,6 +77,7 @@ use core::time::Duration; use crate::sign::EntropySource; use crate::io; use crate::blinded_path::BlindedPath; +use crate::ln::channelmanager::PaymentId; use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::MAX_VALUE_MSAT; @@ -169,7 +170,7 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { secp_ctx: &'a Secp256k1 ) -> Self where ES::Target: EntropySource { let nonce = Nonce::from_entropy_source(entropy_source); - let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None); let metadata = Metadata::DerivedSigningPubkey(derivation_material); OfferBuilder { offer: OfferContents { @@ -283,7 +284,7 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> { let mut tlv_stream = self.offer.as_tlv_stream(); debug_assert_eq!(tlv_stream.metadata, None); tlv_stream.metadata = None; - if metadata.derives_keys() { + if metadata.derives_recipient_keys() { tlv_stream.node_id = None; } @@ -454,10 +455,12 @@ impl Offer { /// Similar to [`Offer::request_invoice`] except it: /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each - /// request, and - /// - sets the [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is - /// called such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice - /// was requested using a base [`ExpandedKey`] from which the payer id was derived. + /// request, + /// - sets [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is called + /// such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was + /// requested using a base [`ExpandedKey`] from which the payer id was derived, and + /// - includes the [`PaymentId`] encrypted in [`InvoiceRequest::payer_metadata`] so that it can + /// be used when sending the payment for the requested invoice. /// /// Useful to protect the sender's privacy. /// @@ -468,7 +471,8 @@ impl Offer { /// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>( - &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1 + &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1, + payment_id: PaymentId ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, @@ -477,7 +481,9 @@ impl Offer { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx)) + Ok(InvoiceRequestBuilder::deriving_payer_id( + self, expanded_key, entropy_source, secp_ctx, payment_id + )) } /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the @@ -489,7 +495,8 @@ impl Offer { /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id pub fn request_invoice_deriving_metadata( - &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES + &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + payment_id: PaymentId ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, @@ -498,7 +505,9 @@ impl Offer { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source)) + Ok(InvoiceRequestBuilder::deriving_metadata( + self, payer_id, expanded_key, entropy_source, payment_id + )) } /// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`, @@ -661,11 +670,13 @@ impl OfferContents { let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| { match record.r#type { OFFER_METADATA_TYPE => false, - OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(), + OFFER_NODE_ID_TYPE => { + !self.metadata.as_ref().unwrap().derives_recipient_keys() + }, _ => true, } }); - signer::verify_metadata( + signer::verify_recipient_metadata( metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx ) }, diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index d419e8fe0..4b4572b4d 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -82,6 +82,7 @@ use crate::sign::EntropySource; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::PaymentHash; +use crate::ln::channelmanager::PaymentId; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; @@ -147,18 +148,22 @@ impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { /// 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`]. /// + /// The `payment_id` is encrypted in the metadata and should be unique. This ensures that only + /// one invoice will be paid for the refund and that payments can be uniquely identified. + /// /// [`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 + secp_ctx: &'a Secp256k1, amount_msats: u64, payment_id: PaymentId ) -> Result where ES::Target: EntropySource { if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } let nonce = Nonce::from_entropy_source(entropy_source); - let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let payment_id = Some(payment_id); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::DerivedSigningPubkey(derivation_material); Ok(Self { refund: RefundContents { @@ -244,7 +249,7 @@ impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { let mut tlv_stream = self.refund.as_tlv_stream(); tlv_stream.0.metadata = None; - if metadata.derives_keys() { + if metadata.derives_payer_keys() { tlv_stream.2.payer_id = None; } @@ -566,7 +571,7 @@ impl RefundContents { } pub(super) fn derives_keys(&self) -> bool { - self.payer.0.derives_keys() + self.payer.0.derives_payer_keys() } pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { @@ -748,6 +753,7 @@ mod tests { use core::time::Duration; use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::sign::KeyMaterial; + use crate::ln::channelmanager::PaymentId; use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; @@ -841,9 +847,10 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); let refund = RefundBuilder - ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000) + ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) .unwrap() .build().unwrap(); assert_eq!(refund.payer_id(), node_id); @@ -854,7 +861,10 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx)); + match invoice.verify(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } let mut tlv_stream = refund.as_tlv_stream(); tlv_stream.2.amount = Some(2000); @@ -867,7 +877,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let mut tlv_stream = refund.as_tlv_stream(); @@ -882,7 +892,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -892,6 +902,7 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); let blinded_path = BlindedPath { introduction_node_id: pubkey(40), @@ -903,7 +914,7 @@ mod tests { }; let refund = RefundBuilder - ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000) + ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) .unwrap() .path(blinded_path) .build().unwrap(); @@ -914,7 +925,10 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx)); + match invoice.verify(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let mut tlv_stream = refund.as_tlv_stream(); @@ -928,7 +942,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer_id let mut tlv_stream = refund.as_tlv_stream(); @@ -943,7 +957,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(!invoice.verify(&expanded_key, &secp_ctx)); + assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 8d5f98e6f..4d5d4662b 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -16,15 +16,26 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self}; use core::convert::TryFrom; use core::fmt; +use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::offers::merkle::TlvRecord; use crate::util::ser::Writeable; use crate::prelude::*; +// Use a different HMAC input for each derivation. Otherwise, an attacker could: +// - take an Offer that has metadata consisting of a nonce and HMAC +// - strip off the HMAC and replace the signing_pubkey where the privkey is the HMAC, +// - generate and sign an invoice using the new signing_pubkey, and +// - claim they paid it since they would know the preimage of the invoice's payment_hash const DERIVED_METADATA_HMAC_INPUT: &[u8; 16] = &[1; 16]; const DERIVED_METADATA_AND_KEYS_HMAC_INPUT: &[u8; 16] = &[2; 16]; +// Additional HMAC inputs to distinguish use cases, either Offer or Refund/InvoiceRequest, where +// metadata for the latter contain an encrypted PaymentId. +const WITHOUT_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[3; 16]; +const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -56,7 +67,20 @@ impl Metadata { } } - pub fn derives_keys(&self) -> bool { + pub fn derives_payer_keys(&self) -> bool { + match self { + // Infer whether Metadata::derived_from was called on Metadata::DerivedSigningPubkey to + // produce Metadata::Bytes. This is merely to determine which fields should be included + // when verifying a message. It doesn't necessarily indicate that keys were in fact + // derived, as wouldn't be the case if a Metadata::Bytes with length PaymentId::LENGTH + + // Nonce::LENGTH had been set explicitly. + Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH, + Metadata::Derived(_) => false, + Metadata::DerivedSigningPubkey(_) => true, + } + } + + pub fn derives_recipient_keys(&self) -> bool { match self { // Infer whether Metadata::derived_from was called on Metadata::DerivedSigningPubkey to // produce Metadata::Bytes. This is merely to determine which fields should be included @@ -132,20 +156,33 @@ impl PartialEq for Metadata { pub(super) struct MetadataMaterial { nonce: Nonce, hmac: HmacEngine, + // Some for payer metadata and None for offer metadata + encrypted_payment_id: Option<[u8; PaymentId::LENGTH]>, } impl MetadataMaterial { - pub fn new(nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN]) -> Self { + pub fn new( + nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + payment_id: Option + ) -> Self { + // Encrypt payment_id + let encrypted_payment_id = payment_id.map(|payment_id| { + expanded_key.crypt_for_offer(payment_id.0, nonce) + }); + Self { nonce, hmac: expanded_key.hmac_for_offer(nonce, iv_bytes), + encrypted_payment_id, } } fn derive_metadata(mut self) -> Vec { self.hmac.input(DERIVED_METADATA_HMAC_INPUT); + self.maybe_include_encrypted_payment_id(); - let mut bytes = self.nonce.as_slice().to_vec(); + let mut bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or(vec![]); + bytes.extend_from_slice(self.nonce.as_slice()); bytes.extend_from_slice(&Hmac::from_engine(self.hmac).into_inner()); bytes } @@ -154,11 +191,26 @@ impl MetadataMaterial { mut self, secp_ctx: &Secp256k1 ) -> (Vec, KeyPair) { self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); + self.maybe_include_encrypted_payment_id(); + + let mut bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or(vec![]); + bytes.extend_from_slice(self.nonce.as_slice()); let hmac = Hmac::from_engine(self.hmac); let privkey = SecretKey::from_slice(hmac.as_inner()).unwrap(); let keys = KeyPair::from_secret_key(secp_ctx, &privkey); - (self.nonce.as_slice().to_vec(), keys) + + (bytes, keys) + } + + fn maybe_include_encrypted_payment_id(&mut self) { + match self.encrypted_payment_id { + None => self.hmac.input(WITHOUT_ENCRYPTED_PAYMENT_ID_HMAC_INPUT), + Some(encrypted_payment_id) => { + self.hmac.input(WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT); + self.hmac.input(&encrypted_payment_id) + }, + } } } @@ -170,19 +222,65 @@ pub(super) fn derive_keys(nonce: Nonce, expanded_key: &ExpandedKey) -> KeyPair { KeyPair::from_secret_key(&secp_ctx, &privkey) } +/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: +/// - a 256-bit [`PaymentId`], +/// - a 128-bit [`Nonce`], and possibly +/// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`]. +/// +/// 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. +/// +/// Returns the [`PaymentId`] that should be used for sending the payment. +pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>( + metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, + secp_ctx: &Secp256k1 +) -> Result { + if metadata.len() < PaymentId::LENGTH { + return Err(()); + } + + let mut encrypted_payment_id = [0u8; PaymentId::LENGTH]; + encrypted_payment_id.copy_from_slice(&metadata[..PaymentId::LENGTH]); + + let mut hmac = hmac_for_message( + &metadata[PaymentId::LENGTH..], expanded_key, iv_bytes, tlv_stream + )?; + hmac.input(WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT); + hmac.input(&encrypted_payment_id); + + verify_metadata( + &metadata[PaymentId::LENGTH..], Hmac::from_engine(hmac), signing_pubkey, secp_ctx + )?; + + let nonce = Nonce::try_from(&metadata[PaymentId::LENGTH..][..Nonce::LENGTH]).unwrap(); + let payment_id = expanded_key.crypt_for_offer(encrypted_payment_id, nonce); + + Ok(PaymentId(payment_id)) +} + /// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: /// - a 128-bit [`Nonce`] and possibly /// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`]. /// /// 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>( +/// +/// Returns the [`KeyPair`] for signing the invoice, if it can be derived from the metadata. +pub(super) fn verify_recipient_metadata<'a, T: secp256k1::Signing>( metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, secp_ctx: &Secp256k1 ) -> Result, ()> { - let hmac = hmac_for_message(metadata, expanded_key, iv_bytes, tlv_stream)?; + let mut hmac = hmac_for_message(metadata, expanded_key, iv_bytes, tlv_stream)?; + hmac.input(WITHOUT_ENCRYPTED_PAYMENT_ID_HMAC_INPUT); + + verify_metadata(metadata, Hmac::from_engine(hmac), signing_pubkey, secp_ctx) +} +fn verify_metadata( + metadata: &[u8], hmac: Hmac, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 +) -> Result, ()> { if metadata.len() == Nonce::LENGTH { let derived_keys = KeyPair::from_secret_key( secp_ctx, &SecretKey::from_slice(hmac.as_inner()).unwrap() @@ -206,7 +304,7 @@ pub(super) fn verify_metadata<'a, T: secp256k1::Signing>( fn hmac_for_message<'a>( metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], tlv_stream: impl core::iter::Iterator> -) -> Result, ()> { +) -> Result, ()> { if metadata.len() < Nonce::LENGTH { return Err(()); } @@ -227,5 +325,5 @@ fn hmac_for_message<'a>( hmac.input(DERIVED_METADATA_HMAC_INPUT); } - Ok(Hmac::from_engine(hmac)) + Ok(hmac) } -- 2.39.5