Skip to content

Commit

Permalink
Merge pull request #3163 from shaavan/invoice_reply_path
Browse files Browse the repository at this point in the history
Introduce Reply Paths for BOLT12 Invoice in Offers Flow.
  • Loading branch information
TheBlueMatt authored Sep 11, 2024
2 parents 82b3f62 + 7b49993 commit 4178dd7
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 15 deletions.
15 changes: 15 additions & 0 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,19 @@ pub enum OffersContext {
///
/// [`Bolt12Invoice::payment_hash`]: crate::offers::invoice::Bolt12Invoice::payment_hash
payment_hash: PaymentHash,

/// A nonce used for authenticating that a received [`InvoiceError`] is for a valid
/// sent [`Bolt12Invoice`].
///
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
nonce: Nonce,

/// Authentication code for the [`PaymentHash`], which should be checked when the context is
/// used to log the received [`InvoiceError`].
///
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
hmac: Hmac<Sha256>,
},
}

Expand All @@ -366,6 +379,8 @@ impl_writeable_tlv_based_enum!(OffersContext,
},
(2, InboundPayment) => {
(0, payment_hash, required),
(1, nonce, required),
(2, hmac, required)
},
);

Expand Down
56 changes: 51 additions & 5 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,38 @@ impl From<&ClaimableHTLC> for events::ClaimedHTLC {
}
}

/// A trait defining behavior for creating and verifing the HMAC for authenticating a given data.
pub trait Verification {
/// Constructs an HMAC to include in [`OffersContext`] for the data along with the given
/// [`Nonce`].
fn hmac_for_offer_payment(
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Hmac<Sha256>;

/// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`].
fn verify(
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Result<(), ()>;
}

impl Verification for PaymentHash {
/// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash
/// along with the given [`Nonce`].
fn hmac_for_offer_payment(
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Hmac<Sha256> {
signer::hmac_for_payment_hash(*self, nonce, expanded_key)
}

/// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an
/// [`OffersContext::InboundPayment`].
fn verify(
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Result<(), ()> {
signer::verify_payment_hash(*self, hmac, nonce, expanded_key)
}
}

/// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify
/// a payment and ensure idempotency in LDK.
///
Expand All @@ -419,18 +451,20 @@ pub struct PaymentId(pub [u8; Self::LENGTH]);
impl PaymentId {
/// Number of bytes in the id.
pub const LENGTH: usize = 32;
}

impl Verification for PaymentId {
/// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id
/// along with the given [`Nonce`].
pub fn hmac_for_offer_payment(
fn hmac_for_offer_payment(
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Hmac<Sha256> {
signer::hmac_for_payment_id(*self, nonce, expanded_key)
}

/// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an
/// [`OffersContext::OutboundPayment`].
pub fn verify(
fn verify(
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
) -> Result<(), ()> {
signer::verify_payment_id(*self, hmac, nonce, expanded_key)
Expand Down Expand Up @@ -9195,8 +9229,10 @@ where
let builder: InvoiceBuilder<DerivedSigningPubkey> = builder.into();
let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?;

let nonce = Nonce::from_entropy_source(entropy);
let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key);
let context = OffersContext::InboundPayment {
payment_hash: invoice.payment_hash(),
payment_hash: invoice.payment_hash(), nonce, hmac
};
let reply_paths = self.create_blinded_paths(context)
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
Expand Down Expand Up @@ -10894,7 +10930,12 @@ where
};

match response {
Ok(invoice) => Some((OffersMessage::Invoice(invoice), responder.respond())),
Ok(invoice) => {
let nonce = Nonce::from_entropy_source(&*self.entropy_source);
let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key);
let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac });
Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context)))
},
Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())),
}
},
Expand Down Expand Up @@ -10956,7 +10997,12 @@ where
},
OffersMessage::InvoiceError(invoice_error) => {
let payment_hash = match context {
Some(OffersContext::InboundPayment { payment_hash }) => Some(payment_hash),
Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => {
match payment_hash.verify(hmac, nonce, expanded_key) {
Ok(_) => Some(payment_hash),
Err(_) => None,
}
},
_ => None,
};

Expand Down
34 changes: 24 additions & 10 deletions lightning/src/ln/offers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,12 @@ fn extract_invoice_request<'a, 'b, 'c>(
}
}

fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, Option<BlindedMessagePath>) {
fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, BlindedMessagePath) {
match node.onion_messenger.peel_onion_message(message) {
Ok(PeeledOnion::Receive(message, _, reply_path)) => match message {
ParsedOnionMessageContents::Offers(offers_message) => match offers_message {
OffersMessage::InvoiceRequest(invoice_request) => panic!("Unexpected invoice_request: {:?}", invoice_request),
OffersMessage::Invoice(invoice) => (invoice, reply_path),
OffersMessage::Invoice(invoice) => (invoice, reply_path.unwrap()),
#[cfg(async_payments)]
OffersMessage::StaticInvoice(invoice) => panic!("Unexpected static invoice: {:?}", invoice),
OffersMessage::InvoiceError(error) => panic!("Unexpected invoice_error: {:?}", error),
Expand Down Expand Up @@ -580,13 +580,22 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap();
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);

let (invoice, _) = extract_invoice(david, &onion_message);
let (invoice, reply_path) = extract_invoice(david, &onion_message);
assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
}
// Both Bob and Charlie have an equal number of channels and need to be connected
// to Alice when she's handling the message. Therefore, either Bob or Charlie could
// serve as the introduction node for the reply path back to Alice.
assert!(
matches!(
reply_path.introduction_node(),
&IntroductionNode::NodeId(node_id) if node_id == bob_id || node_id == charlie_id,
)
);

route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
Expand Down Expand Up @@ -659,7 +668,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap();
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);

let (invoice, _) = extract_invoice(david, &onion_message);
let (invoice, reply_path) = extract_invoice(david, &onion_message);
assert_eq!(invoice, expected_invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
Expand All @@ -668,6 +677,8 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
}
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));


route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
Expand Down Expand Up @@ -726,13 +737,14 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let (invoice, _) = extract_invoice(bob, &onion_message);
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
}
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id));

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
Expand Down Expand Up @@ -779,7 +791,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let (invoice, _) = extract_invoice(bob, &onion_message);
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
assert_eq!(invoice, expected_invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
Expand All @@ -788,6 +800,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
}
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id));

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
Expand Down Expand Up @@ -1044,7 +1057,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() {
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();

let (_, reply_path) = extract_invoice(alice, &onion_message);
assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(charlie_id));
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));

// Send, extract and verify the second Invoice Request message
let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
Expand All @@ -1053,7 +1066,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() {
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();

let (_, reply_path) = extract_invoice(alice, &onion_message);
assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id()));
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id()));
}

/// Checks that a deferred invoice can be paid asynchronously from an Event::InvoiceReceived.
Expand Down Expand Up @@ -1190,12 +1203,13 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let (invoice, _) = extract_invoice(bob, &onion_message);
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
for path in invoice.payment_paths() {
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
}
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
Expand Down Expand Up @@ -1239,7 +1253,7 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() {

let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();

let (invoice, _) = extract_invoice(bob, &onion_message);
let (invoice, _reply_path) = extract_invoice(bob, &onion_message);
assert_eq!(invoice, expected_invoice);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
Expand Down
23 changes: 23 additions & 0 deletions lightning/src/offers/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use bitcoin::hashes::cmp::fixed_time_eq;
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey, self};
use types::payment::PaymentHash;
use core::fmt;
use crate::ln::channelmanager::PaymentId;
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
Expand All @@ -39,6 +40,9 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16];
// HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`.
const PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16];

// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`.
const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16];

/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
/// verified.
#[derive(Clone)]
Expand Down Expand Up @@ -413,3 +417,22 @@ pub(crate) fn verify_payment_id(
) -> Result<(), ()> {
if hmac_for_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
}

pub(crate) fn hmac_for_payment_hash(
payment_hash: PaymentHash, nonce: Nonce, expanded_key: &ExpandedKey,
) -> Hmac<Sha256> {
const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment Hash";
let mut hmac = expanded_key.hmac_for_offer();
hmac.input(IV_BYTES);
hmac.input(&nonce.0);
hmac.input(PAYMENT_HASH_HMAC_INPUT);
hmac.input(&payment_hash.0);

Hmac::from_engine(hmac)
}

pub(crate) fn verify_payment_hash(
payment_hash: PaymentHash, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &ExpandedKey,
) -> Result<(), ()> {
if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
}

0 comments on commit 4178dd7

Please sign in to comment.