Skip to content

Commit

Permalink
Include PaymentId in payer metadata
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jkczyz committed Aug 29, 2023
1 parent 861e0ee commit 7a3e06b
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 54 deletions.
7 changes: 6 additions & 1 deletion lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
Expand Down
7 changes: 7 additions & 0 deletions lightning/src/ln/inbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 6 additions & 7 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<T: secp256k1::Signing>(
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
) -> bool {
) -> Result<PaymentId, ()> {
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
}

Expand Down Expand Up @@ -947,7 +949,7 @@ impl InvoiceContents {

fn verify<T: secp256k1::Signing>(
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
) -> bool {
) -> Result<PaymentId, ()> {
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
match record.r#type {
Expand All @@ -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 {
Expand Down
42 changes: 28 additions & 14 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -128,10 +129,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
}

pub(super) fn deriving_metadata<ES: Deref>(
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,
Expand All @@ -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<ES: Deref>(
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES,
secp_ctx: &'b Secp256k1<T>, 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,
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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();
Expand All @@ -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 (
Expand All @@ -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 (
Expand All @@ -1130,20 +1140,21 @@ 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]
fn builds_invoice_request_with_derived_payer_id() {
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
let entropy = FixedEntropy {};
let secp_ctx = Secp256k1::new();
let 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();
Expand All @@ -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 (
Expand All @@ -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 (
Expand All @@ -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]
Expand Down
35 changes: 23 additions & 12 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -169,7 +170,7 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
secp_ctx: &'a Secp256k1<T>
) -> 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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
///
Expand All @@ -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<T>
&'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>,
payment_id: PaymentId
) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, Bolt12SemanticError>
where
ES::Target: EntropySource,
Expand All @@ -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
Expand All @@ -489,7 +495,8 @@ impl Offer {
///
/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
pub fn request_invoice_deriving_metadata<ES: Deref>(
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
payment_id: PaymentId
) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError>
where
ES::Target: EntropySource,
Expand All @@ -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`,
Expand Down Expand Up @@ -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
)
},
Expand Down
Loading

0 comments on commit 7a3e06b

Please sign in to comment.