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

Offers: invoice verification #89

Merged
merged 3 commits into from
Mar 7, 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
30 changes: 26 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ use crate::lnd::{
use crate::lndk_offers::OfferError;
use crate::onion_messenger::MessengerUtilities;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey};
use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey, Secp256k1};
use home::home_dir;
use lightning::blinded_path::BlindedPath;
use lightning::ln::inbound_payment::ExpandedKey;
use lightning::ln::peer_handler::IgnoringMessageHandler;
use lightning::offers::invoice_error::InvoiceError;
use lightning::offers::offer::Offer;
use lightning::onion_message::{
DefaultMessageRouter, Destination, OffersMessage, OffersMessageHandler, OnionMessenger,
PendingOnionMessage,
};
use lightning::sign::{EntropySource, KeyMaterial};
use log::{error, info, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
Expand Down Expand Up @@ -182,7 +185,8 @@ enum OfferState {
pub struct OfferHandler {
active_offers: Mutex<HashMap<String, OfferState>>,
pending_messages: Mutex<Vec<PendingOnionMessage<OffersMessage>>>,
messenger_utils: MessengerUtilities,
pub messenger_utils: MessengerUtilities,
expanded_key: ExpandedKey,
}

#[derive(Clone)]
Expand All @@ -199,10 +203,15 @@ pub struct PayOfferParams {

impl OfferHandler {
pub fn new() -> Self {
let messenger_utils = MessengerUtilities::new();
let random_bytes = messenger_utils.get_secure_random_bytes();
let expanded_key = ExpandedKey::new(&KeyMaterial(random_bytes));

OfferHandler {
active_offers: Mutex::new(HashMap::new()),
pending_messages: Mutex::new(Vec::new()),
messenger_utils: MessengerUtilities::new(),
messenger_utils,
expanded_key,
}
}

Expand Down Expand Up @@ -230,7 +239,20 @@ impl OffersMessageHandler for OfferHandler {
log::error!("Invoice request received, payment not yet supported.");
None
}
OffersMessage::Invoice(_invoice) => None,
OffersMessage::Invoice(invoice) => {
let secp_ctx = &Secp256k1::new();
// We verify that this invoice is a response to the invoice request we just sent.
match invoice.verify(&self.expanded_key, secp_ctx) {
// TODO: Eventually when we allow for multiple payments in flight, we can use the
// returned payment id below to check if we already processed an invoice for
// this payment. Right now it's safe to let this be because we won't try to pay
// a second invoice (if it comes through).
Ok(_payment_id) => Some(OffersMessage::Invoice(invoice)),
Err(()) => Some(OffersMessage::InvoiceError(InvoiceError::from_string(
String::from("invoice verification failure"),
))),
}
}
OffersMessage::InvoiceError(_error) => None,
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/lnd.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::OfferError;
use async_trait::async_trait;
use bitcoin::bech32::u5;
use bitcoin::hashes::sha256::Hash;
Expand All @@ -8,7 +9,7 @@ use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1};
use futures::executor::block_on;
use lightning::ln::msgs::UnsignedGossipMessage;
use lightning::offers::invoice::UnsignedBolt12Invoice;
use lightning::offers::invoice_request::UnsignedInvoiceRequest;
use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest};
use lightning::sign::{KeyMaterial, NodeSigner, Recipient};
use std::cell::RefCell;
use std::collections::HashMap;
Expand Down Expand Up @@ -195,6 +196,11 @@ pub trait MessageSigner {
merkle_hash: Hash,
tag: String,
) -> Result<Vec<u8>, Status>;
fn sign_uir(
&mut self,
key_loc: KeyLocator,
unsigned_invoice_req: UnsignedInvoiceRequest,
) -> Result<InvoiceRequest, OfferError<bitcoin::secp256k1::Error>>;
}

/// PeerConnector provides a layer of abstraction over the LND API for connecting to a peer.
Expand Down
134 changes: 85 additions & 49 deletions src/lndk_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ use bitcoin::secp256k1::schnorr::Signature;
use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey, Secp256k1};
use futures::executor::block_on;
use lightning::blinded_path::BlindedPath;
use lightning::ln::channelmanager::PaymentId;
use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest};
use lightning::offers::merkle::SignError;
use lightning::offers::offer::{Amount, Offer};
use lightning::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
use lightning::onion_message::{Destination, OffersMessage, PendingOnionMessage};
use lightning::sign::EntropySource;
use log::error;
use std::error::Error;
use std::fmt::Display;
Expand Down Expand Up @@ -151,7 +153,7 @@ impl OfferHandler {
&self,
mut signer: impl MessageSigner + std::marker::Send + 'static,
offer: Offer,
metadata: Vec<u8>,
_metadata: Vec<u8>,
network: Network,
msats: u64,
) -> Result<InvoiceRequest, OfferError<bitcoin::secp256k1::Error>> {
Expand All @@ -169,8 +171,19 @@ impl OfferHandler {
let pubkey =
PublicKey::from_slice(&pubkey_bytes).expect("failed to deserialize public key");

// Generate a new payment id for this payment.
let bytes = self.messenger_utils.get_secure_random_bytes();
// We need to add some metadata to the invoice request to help with verification of the invoice
// once returned from the offer maker. Once we get an invoice back, this metadata will help us
// to determine: 1) That the invoice is truly for the invoice request we sent. 2) We don't pay
// duplicate invoices.
let unsigned_invoice_req = offer
.request_invoice(metadata, pubkey)
.request_invoice_deriving_metadata(
pubkey,
&self.expanded_key,
&self.messenger_utils,
PaymentId(bytes),
)
.unwrap()
.chain(network)
.unwrap()
Expand All @@ -182,24 +195,9 @@ impl OfferHandler {
// To create a valid invoice request, we also need to sign it. This is spawned in a blocking
// task because we need to call block_on on sign_message so that sign_closure can be a
// synchronous closure.
task::spawn_blocking(move || {
let sign_closure = |msg: &UnsignedInvoiceRequest| {
let tagged_hash = msg.as_ref();
let tag = tagged_hash.tag().to_string();

let signature =
block_on(signer.sign_message(key_loc, tagged_hash.merkle_root(), tag))
.map_err(|_| Secp256k1Error::InvalidSignature)?;

Signature::from_slice(&signature)
};

unsigned_invoice_req
.sign(sign_closure)
.map_err(OfferError::SignError)
})
.await
.unwrap()
task::spawn_blocking(move || signer.sign_uir(key_loc, unsigned_invoice_req))
.await
.unwrap()
}

/// create_reply_path creates a blinded path to provide to the offer maker when requesting an
Expand Down Expand Up @@ -349,7 +347,6 @@ impl PeerConnector for Client {
let list_req = ListPeersRequest {
..Default::default()
};

self.lightning()
.list_peers(list_req)
.await
Expand Down Expand Up @@ -416,12 +413,34 @@ impl MessageSigner for Client {
let resp_inner = resp.into_inner();
Ok(resp_inner.signature)
}

fn sign_uir(
&mut self,
key_loc: KeyLocator,
unsigned_invoice_req: UnsignedInvoiceRequest,
) -> Result<InvoiceRequest, OfferError<bitcoin::secp256k1::Error>> {
let sign_closure = |msg: &UnsignedInvoiceRequest| {
let tagged_hash = msg.as_ref();
let tag = tagged_hash.tag().to_string();

let signature = block_on(self.sign_message(key_loc, tagged_hash.merkle_root(), tag))
.map_err(|_| Secp256k1Error::InvalidSignature)?;

Signature::from_slice(&signature)
};

unsigned_invoice_req
.sign(sign_closure)
.map_err(OfferError::SignError)
}
}

#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use bitcoin::secp256k1::{Error as Secp256k1Error, KeyPair, Secp256k1, SecretKey};
use core::convert::Infallible;
use lightning::offers::merkle::SignError;
use lightning::offers::offer::{OfferBuilder, Quantity};
use mockall::mock;
use std::collections::HashMap;
Expand Down Expand Up @@ -452,8 +471,23 @@ mod tests {
"0313ba7ccbd754c117962b9afab6c2870eb3ef43f364a9f6c43d0fabb4553776ba".to_string()
}

fn get_signature() -> String {
"28b937976a29c15827433086440b36c2bec6ca5bd977557972dca8641cd59ffba50daafb8ee99a19c950976b46f47d9e7aa716652e5657dfc555b82eff467f18".to_string()
fn get_invoice_request(offer: Offer, amount: u64) -> InvoiceRequest {
let secp_ctx = Secp256k1::new();
let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
let pubkey = PublicKey::from(keys);
offer
.request_invoice(vec![42; 64], pubkey)
.unwrap()
.chain(Network::Regtest)
.unwrap()
.amount_msats(amount)
.unwrap()
.build()
.unwrap()
.sign::<_, Infallible>(|message| {
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
})
.expect("failed verifying signature")
}

mock! {
Expand All @@ -463,6 +497,7 @@ mod tests {
impl MessageSigner for TestBolt12Signer {
async fn derive_key(&mut self, key_loc: KeyLocator) -> Result<Vec<u8>, Status>;
async fn sign_message(&mut self, key_loc: KeyLocator, merkle_hash: Hash, tag: String) -> Result<Vec<u8>, Status>;
fn sign_uir(&mut self, key_loc: KeyLocator, unsigned_invoice_req: UnsignedInvoiceRequest) -> Result<InvoiceRequest, OfferError<bitcoin::secp256k1::Error>>;
}
}

Expand All @@ -482,25 +517,27 @@ mod tests {
let mut signer_mock = MockTestBolt12Signer::new();

signer_mock.expect_derive_key().returning(|_| {
Ok(PublicKey::from_str(&get_pubkey())
.unwrap()
.serialize()
.to_vec())
let pubkey = PublicKey::from_str(&get_pubkey()).unwrap();
Ok(pubkey.serialize().to_vec())
});

signer_mock.expect_sign_message().returning(|_, _, _| {
Ok(Signature::from_str(&get_signature())
.unwrap()
.as_ref()
.to_vec())
});
let offer = decode(get_offer()).unwrap();
let offer_amount = offer.amount().unwrap();
let amount = match offer_amount {
Amount::Bitcoin { amount_msats } => *amount_msats,
_ => panic!("unexpected amount type"),
};

signer_mock
.expect_sign_uir()
.returning(move |_, _| Ok(get_invoice_request(offer.clone(), amount)));

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.await
.is_ok())
let resp = handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, amount)
.await;
assert!(resp.is_ok())
}

#[tokio::test]
Expand All @@ -511,17 +548,14 @@ mod tests {
.expect_derive_key()
.returning(|_| Err(Status::unknown("error testing")));

signer_mock.expect_sign_message().returning(|_, _, _| {
Ok(Signature::from_str(&get_signature())
.unwrap()
.as_ref()
.to_vec())
});
signer_mock
.expect_sign_uir()
.returning(move |_, _| Ok(get_invoice_request(decode(get_offer()).unwrap(), 10000)));

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000,)
.await
.is_err())
}
Expand All @@ -537,14 +571,16 @@ mod tests {
.to_vec())
});

signer_mock
.expect_sign_message()
.returning(|_, _, _| Err(Status::unknown("error testing")));
signer_mock.expect_sign_uir().returning(move |_, _| {
Err(OfferError::SignError(SignError::Signing(
Secp256k1Error::InvalidSignature,
)))
});

let offer = decode(get_offer()).unwrap();
let handler = OfferHandler::new();
assert!(handler
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000)
.create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000,)
.await
.is_err())
}
Expand Down
Loading