Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Reply Paths for BOLT12 Invoice in Offers Flow. #3163

Merged
merged 5 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
shaavan marked this conversation as resolved.
Show resolved Hide resolved
},
);

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 @@ -9192,8 +9226,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 @@ -10891,7 +10927,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 @@ -10953,7 +10994,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(()) }
}
Loading