Skip to content

Commit 673541e

Browse files
committed
Stateless verification of Invoice for Refund
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.
1 parent d5dd5eb commit 673541e

File tree

3 files changed

+108
-7
lines changed

3 files changed

+108
-7
lines changed

lightning/src/offers/invoice.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,9 @@ impl InvoiceContents {
524524
InvoiceContents::ForOffer { invoice_request, .. } => {
525525
invoice_request.verify(tlv_stream, key)
526526
},
527-
_ => todo!(),
527+
InvoiceContents::ForRefund { refund, .. } => {
528+
refund.verify(tlv_stream, key)
529+
},
528530
}
529531
}
530532

lightning/src/offers/invoice_request.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,12 +498,12 @@ impl Writeable for InvoiceRequestContents {
498498
}
499499

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

503503
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
504504
///
505505
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
506-
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
506+
pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
507507

508508
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
509509
(80, chain: ChainHash),

lightning/src/offers/refund.rs

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@ use crate::ln::features::InvoiceRequestFeatures;
8383
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
8484
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
8585
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
86-
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
87-
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
86+
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
87+
use crate::offers::merkle::TlvStream;
88+
use crate::offers::offer::{OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef};
8889
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
89-
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
90-
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
90+
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
91+
use crate::offers::signer::{MetadataMaterial, DerivedPubkey, self};
9192
use crate::onion_message::BlindedPath;
9293
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
9394
use crate::util::string::PrintableString;
@@ -461,6 +462,20 @@ impl RefundContents {
461462
ChainHash::using_genesis_block(Network::Bitcoin)
462463
}
463464

465+
/// Verifies that the payer metadata was produced from the refund in the TLV stream.
466+
pub(super) fn verify(&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey) -> bool {
467+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
468+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
469+
match record.r#type {
470+
PAYER_METADATA_TYPE => false, // Should be outside range
471+
INVOICE_REQUEST_PAYER_ID_TYPE => false,
472+
_ => true,
473+
}
474+
});
475+
let tlv_stream = offer_records.chain(invreq_records);
476+
signer::verify_metadata(&self.payer.0, key, tlv_stream)
477+
}
478+
464479
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
465480
let payer = PayerTlvStreamRef {
466481
metadata: Some(&self.payer.0),
@@ -638,12 +653,15 @@ mod tests {
638653
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
639654
use core::convert::TryFrom;
640655
use core::time::Duration;
656+
use crate::chain::keysinterface::KeyMaterial;
641657
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
658+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
642659
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
643660
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
644661
use crate::offers::offer::OfferTlvStreamRef;
645662
use crate::offers::parse::{ParseError, SemanticError};
646663
use crate::offers::payer::PayerTlvStreamRef;
664+
use crate::offers::signer::DerivedPubkey;
647665
use crate::offers::test_utils::*;
648666
use crate::onion_message::{BlindedHop, BlindedPath};
649667
use crate::util::ser::{BigSize, Writeable};
@@ -724,6 +742,87 @@ mod tests {
724742
}
725743
}
726744

745+
#[test]
746+
fn builds_refund_with_metadata_derived() {
747+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
748+
let nonce = Nonce([42; Nonce::LENGTH]);
749+
750+
let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
751+
.metadata_derived(&expanded_key, nonce).unwrap()
752+
.build().unwrap();
753+
assert_eq!(refund.metadata()[..Nonce::LENGTH], nonce.0);
754+
assert_eq!(refund.payer_id(), payer_pubkey());
755+
756+
let invoice = refund
757+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
758+
.unwrap()
759+
.build().unwrap()
760+
.sign(recipient_sign).unwrap();
761+
assert!(invoice.verify(&expanded_key));
762+
763+
let mut tlv_stream = refund.as_tlv_stream();
764+
tlv_stream.2.amount = Some(2000);
765+
766+
let mut encoded_refund = Vec::new();
767+
tlv_stream.write(&mut encoded_refund).unwrap();
768+
769+
let invoice = Refund::try_from(encoded_refund).unwrap()
770+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
771+
.unwrap()
772+
.build().unwrap()
773+
.sign(recipient_sign).unwrap();
774+
assert!(!invoice.verify(&expanded_key));
775+
776+
match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
777+
.metadata_derived(&expanded_key, nonce).unwrap()
778+
.metadata_derived(&expanded_key, nonce)
779+
{
780+
Ok(_) => panic!("expected error"),
781+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
782+
}
783+
}
784+
785+
#[test]
786+
fn builds_refund_with_derived_payer_id() {
787+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
788+
let nonce = Nonce([42; Nonce::LENGTH]);
789+
let keys = expanded_key.signing_keypair_for_offer(nonce);
790+
791+
let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
792+
let refund = RefundBuilder::deriving_payer_id("foo".into(), payer_pubkey, 1000).unwrap()
793+
.build().unwrap();
794+
assert_eq!(refund.metadata()[..Nonce::LENGTH], nonce.0);
795+
assert_eq!(refund.payer_id(), keys.public_key());
796+
797+
let invoice = refund
798+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
799+
.unwrap()
800+
.build().unwrap()
801+
.sign(recipient_sign).unwrap();
802+
assert!(invoice.verify(&expanded_key));
803+
804+
let mut tlv_stream = refund.as_tlv_stream();
805+
tlv_stream.2.amount = Some(2000);
806+
807+
let mut encoded_refund = Vec::new();
808+
tlv_stream.write(&mut encoded_refund).unwrap();
809+
810+
let invoice = Refund::try_from(encoded_refund).unwrap()
811+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
812+
.unwrap()
813+
.build().unwrap()
814+
.sign(recipient_sign).unwrap();
815+
assert!(!invoice.verify(&expanded_key));
816+
817+
let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
818+
match RefundBuilder::deriving_payer_id("foo".into(), payer_pubkey, 1000).unwrap()
819+
.metadata_derived(&expanded_key, nonce)
820+
{
821+
Ok(_) => panic!("expected error"),
822+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
823+
}
824+
}
825+
727826
#[test]
728827
fn builds_refund_with_absolute_expiry() {
729828
let future_expiry = Duration::from_secs(u64::max_value());

0 commit comments

Comments
 (0)