Skip to content

Commit 7d71c75

Browse files
committed
Stateless verification of Invoice for Refund
Stateless verification of Invoice for Offer 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 encrypted nonce and possibly a 256-bit HMAC over the nonce and Refund TLV records (excluding the payer id) using an ExpandedKey. Thus, the HMAC can be reproduced from the refund bytes using the nonce and the original ExpandedKey, and then checked against the metadata. If metadata does not contain an HMAC, then the reproduced HMAC was used to form the signing keys, and thus can be checked against the payer id.
1 parent bbdc53a commit 7d71c75

File tree

3 files changed

+145
-8
lines changed

3 files changed

+145
-8
lines changed

lightning/src/offers/invoice.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,9 @@ impl InvoiceContents {
535535
InvoiceContents::ForOffer { invoice_request, .. } => {
536536
invoice_request.verify(tlv_stream, key, secp_ctx)
537537
},
538-
_ => todo!(),
538+
InvoiceContents::ForRefund { refund, .. } => {
539+
refund.verify(tlv_stream, key, secp_ctx)
540+
},
539541
}
540542
}
541543

lightning/src/offers/invoice_request.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,12 +599,12 @@ impl Writeable for InvoiceRequestContents {
599599
}
600600

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

604604
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
605605
///
606606
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
607-
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
607+
pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
608608

609609
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
610610
(80, chain: ChainHash),

lightning/src/offers/refund.rs

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ use crate::ln::features::InvoiceRequestFeatures;
8585
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
8686
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
8787
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
88-
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
89-
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
88+
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
89+
use crate::offers::merkle::TlvStream;
90+
use crate::offers::offer::{OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef};
9091
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
91-
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
92-
use crate::offers::signer::{Metadata, MetadataMaterial};
92+
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
93+
use crate::offers::signer::{Metadata, MetadataMaterial, self};
9394
use crate::onion_message::BlindedPath;
9495
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
9596
use crate::util::string::PrintableString;
@@ -341,7 +342,7 @@ impl Refund {
341342
///
342343
/// [`payer_id`]: Self::payer_id
343344
pub fn metadata(&self) -> &[u8] {
344-
self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
345+
self.contents.metadata()
345346
}
346347

347348
/// A chain that the refund is valid for.
@@ -453,6 +454,10 @@ impl RefundContents {
453454
}
454455
}
455456

457+
fn metadata(&self) -> &[u8] {
458+
self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
459+
}
460+
456461
pub(super) fn chain(&self) -> ChainHash {
457462
self.chain.unwrap_or_else(|| self.implied_chain())
458463
}
@@ -461,6 +466,22 @@ impl RefundContents {
461466
ChainHash::using_genesis_block(Network::Bitcoin)
462467
}
463468

469+
/// Verifies that the payer metadata was produced from the refund in the TLV stream.
470+
pub(super) fn verify<T: secp256k1::Signing>(
471+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
472+
) -> bool {
473+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
474+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
475+
match record.r#type {
476+
PAYER_METADATA_TYPE => false, // Should be outside range
477+
INVOICE_REQUEST_PAYER_ID_TYPE => false,
478+
_ => true,
479+
}
480+
});
481+
let tlv_stream = offer_records.chain(invreq_records);
482+
signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx)
483+
}
484+
464485
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
465486
let payer = PayerTlvStreamRef {
466487
metadata: self.payer.0.as_bytes(),
@@ -638,7 +659,9 @@ mod tests {
638659
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
639660
use core::convert::TryFrom;
640661
use core::time::Duration;
662+
use crate::chain::keysinterface::KeyMaterial;
641663
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
664+
use crate::ln::inbound_payment::ExpandedKey;
642665
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
643666
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
644667
use crate::offers::offer::OfferTlvStreamRef;
@@ -724,6 +747,118 @@ mod tests {
724747
}
725748
}
726749

750+
#[test]
751+
fn builds_refund_with_metadata_derived() {
752+
let desc = "foo".to_string();
753+
let node_id = payer_pubkey();
754+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
755+
let entropy = FixedEntropy {};
756+
let secp_ctx = Secp256k1::new();
757+
758+
let refund = RefundBuilder
759+
::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
760+
.unwrap()
761+
.build().unwrap();
762+
assert_eq!(refund.payer_id(), node_id);
763+
764+
// Fails verification with altered fields
765+
let invoice = refund
766+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
767+
.unwrap()
768+
.build().unwrap()
769+
.sign(recipient_sign).unwrap();
770+
assert!(invoice.verify(&expanded_key, &secp_ctx));
771+
772+
let mut tlv_stream = refund.as_tlv_stream();
773+
tlv_stream.2.amount = Some(2000);
774+
775+
let mut encoded_refund = Vec::new();
776+
tlv_stream.write(&mut encoded_refund).unwrap();
777+
778+
let invoice = Refund::try_from(encoded_refund).unwrap()
779+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
780+
.unwrap()
781+
.build().unwrap()
782+
.sign(recipient_sign).unwrap();
783+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
784+
785+
// Fails verification with altered metadata
786+
let mut tlv_stream = refund.as_tlv_stream();
787+
let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect();
788+
tlv_stream.0.metadata = Some(&metadata);
789+
790+
let mut encoded_refund = Vec::new();
791+
tlv_stream.write(&mut encoded_refund).unwrap();
792+
793+
let invoice = Refund::try_from(encoded_refund).unwrap()
794+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
795+
.unwrap()
796+
.build().unwrap()
797+
.sign(recipient_sign).unwrap();
798+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
799+
}
800+
801+
#[test]
802+
fn builds_refund_with_derived_payer_id() {
803+
let desc = "foo".to_string();
804+
let node_id = payer_pubkey();
805+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
806+
let entropy = FixedEntropy {};
807+
let secp_ctx = Secp256k1::new();
808+
809+
let blinded_path = BlindedPath {
810+
introduction_node_id: pubkey(40),
811+
blinding_point: pubkey(41),
812+
blinded_hops: vec![
813+
BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
814+
BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
815+
],
816+
};
817+
818+
let refund = RefundBuilder
819+
::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
820+
.unwrap()
821+
.path(blinded_path)
822+
.build().unwrap();
823+
assert_ne!(refund.payer_id(), node_id);
824+
825+
let invoice = refund
826+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
827+
.unwrap()
828+
.build().unwrap()
829+
.sign(recipient_sign).unwrap();
830+
assert!(invoice.verify(&expanded_key, &secp_ctx));
831+
832+
// Fails verification with altered fields
833+
let mut tlv_stream = refund.as_tlv_stream();
834+
tlv_stream.2.amount = Some(2000);
835+
836+
let mut encoded_refund = Vec::new();
837+
tlv_stream.write(&mut encoded_refund).unwrap();
838+
839+
let invoice = Refund::try_from(encoded_refund).unwrap()
840+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
841+
.unwrap()
842+
.build().unwrap()
843+
.sign(recipient_sign).unwrap();
844+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
845+
846+
// Fails verification with altered payer_id
847+
let mut tlv_stream = refund.as_tlv_stream();
848+
let payer_id = pubkey(1);
849+
tlv_stream.2.payer_id = Some(&payer_id);
850+
851+
let mut encoded_refund = Vec::new();
852+
tlv_stream.write(&mut encoded_refund).unwrap();
853+
854+
let invoice = Refund::try_from(encoded_refund).unwrap()
855+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
856+
.unwrap()
857+
.build().unwrap()
858+
.sign(recipient_sign).unwrap();
859+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
860+
}
861+
727862
#[test]
728863
fn builds_refund_with_absolute_expiry() {
729864
let future_expiry = Duration::from_secs(u64::max_value());

0 commit comments

Comments
 (0)