Skip to content

Commit

Permalink
Stateless verification of Invoice for Refund
Browse files Browse the repository at this point in the history
Verify that an Invoice was produced from a Refund constructed by the
payer using the payer metadata reflected in the Invoice. The payer
metadata consists of a 128-bit nonce and a 256-bit HMAC over the nonce
and Refund TLV records (excluding the payer id). Thus, the HMAC can be
reproduced using the nonce and the ExpandedKey used to produce the HMAC,
and then checked against the metadata.
  • Loading branch information
jkczyz committed Feb 24, 2023
1 parent 6fdd435 commit 0c52188
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 7 deletions.
4 changes: 3 additions & 1 deletion lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,9 @@ impl InvoiceContents {
InvoiceContents::ForOffer { invoice_request, .. } => {
invoice_request.verify(tlv_stream, key)
},
_ => todo!(),
InvoiceContents::ForRefund { refund, .. } => {
refund.verify(tlv_stream, key)
},
}
}

Expand Down
4 changes: 2 additions & 2 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,12 @@ impl Writeable for InvoiceRequestContents {
}

/// Valid type range for invoice_request TLV records.
const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
pub(super) const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;

/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
///
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;

tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
(80, chain: ChainHash),
Expand Down
107 changes: 103 additions & 4 deletions lightning/src/offers/refund.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
use crate::offers::merkle::TlvStream;
use crate::offers::offer::{OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
use crate::offers::signer::{MetadataMaterial, DerivedPubkey, self};
use crate::onion_message::BlindedPath;
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
Expand Down Expand Up @@ -461,6 +462,20 @@ impl RefundContents {
ChainHash::using_genesis_block(Network::Bitcoin)
}

/// Verifies that the payer metadata was produced from the refund in the TLV stream.
pub(super) fn verify(&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey) -> bool {
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
match record.r#type {
PAYER_METADATA_TYPE => false, // Should be outside range
INVOICE_REQUEST_PAYER_ID_TYPE => false,
_ => true,
}
});
let tlv_stream = offer_records.chain(invreq_records);
signer::verify_metadata(&self.payer.0, key, tlv_stream)
}

pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
let payer = PayerTlvStreamRef {
metadata: Some(&self.payer.0),
Expand Down Expand Up @@ -638,12 +653,15 @@ mod tests {
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use core::convert::TryFrom;
use core::time::Duration;
use crate::chain::keysinterface::KeyMaterial;
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
use crate::offers::offer::OfferTlvStreamRef;
use crate::offers::parse::{ParseError, SemanticError};
use crate::offers::payer::PayerTlvStreamRef;
use crate::offers::signer::DerivedPubkey;
use crate::offers::test_utils::*;
use crate::onion_message::{BlindedHop, BlindedPath};
use crate::util::ser::{BigSize, Writeable};
Expand Down Expand Up @@ -724,6 +742,87 @@ mod tests {
}
}

#[test]
fn builds_refund_with_metadata_derived() {
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
let nonce = Nonce([42; Nonce::LENGTH]);

let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
.metadata_derived(&expanded_key, nonce).unwrap()
.build().unwrap();
assert_eq!(refund.metadata()[..Nonce::LENGTH], nonce.0);
assert_eq!(refund.payer_id(), payer_pubkey());

let invoice = refund
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
.unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
assert!(invoice.verify(&expanded_key));

let mut tlv_stream = refund.as_tlv_stream();
tlv_stream.2.amount = Some(2000);

let mut encoded_refund = Vec::new();
tlv_stream.write(&mut encoded_refund).unwrap();

let invoice = Refund::try_from(encoded_refund).unwrap()
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
.unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
assert!(!invoice.verify(&expanded_key));

match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
.metadata_derived(&expanded_key, nonce).unwrap()
.metadata_derived(&expanded_key, nonce)
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
}
}

#[test]
fn builds_refund_with_derived_payer_id() {
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
let nonce = Nonce([42; Nonce::LENGTH]);
let keys = expanded_key.signing_keypair_for_offer(nonce);

let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
let refund = RefundBuilder::deriving_payer_id("foo".into(), payer_pubkey, 1000).unwrap()
.build().unwrap();
assert_eq!(refund.metadata()[..Nonce::LENGTH], nonce.0);
assert_eq!(refund.payer_id(), keys.public_key());

let invoice = refund
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
.unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
assert!(invoice.verify(&expanded_key));

let mut tlv_stream = refund.as_tlv_stream();
tlv_stream.2.amount = Some(2000);

let mut encoded_refund = Vec::new();
tlv_stream.write(&mut encoded_refund).unwrap();

let invoice = Refund::try_from(encoded_refund).unwrap()
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
.unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
assert!(!invoice.verify(&expanded_key));

let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
match RefundBuilder::deriving_payer_id("foo".into(), payer_pubkey, 1000).unwrap()
.metadata_derived(&expanded_key, nonce)
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
}
}

#[test]
fn builds_refund_with_absolute_expiry() {
let future_expiry = Duration::from_secs(u64::max_value());
Expand Down

0 comments on commit 0c52188

Please sign in to comment.