Skip to content

Commit c0ddf3c

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 5800680 commit c0ddf3c

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
@@ -602,12 +602,12 @@ impl Writeable for InvoiceRequestContents {
602602
}
603603

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

607607
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
608608
///
609609
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
610-
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
610+
pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
611611

612612
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
613613
(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;
@@ -343,7 +344,7 @@ impl Refund {
343344
///
344345
/// [`payer_id`]: Self::payer_id
345346
pub fn metadata(&self) -> &[u8] {
346-
self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
347+
self.contents.metadata()
347348
}
348349

349350
/// A chain that the refund is valid for.
@@ -455,6 +456,10 @@ impl RefundContents {
455456
}
456457
}
457458

459+
fn metadata(&self) -> &[u8] {
460+
self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
461+
}
462+
458463
pub(super) fn chain(&self) -> ChainHash {
459464
self.chain.unwrap_or_else(|| self.implied_chain())
460465
}
@@ -463,6 +468,22 @@ impl RefundContents {
463468
ChainHash::using_genesis_block(Network::Bitcoin)
464469
}
465470

471+
/// Verifies that the payer metadata was produced from the refund in the TLV stream.
472+
pub(super) fn verify<T: secp256k1::Signing>(
473+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
474+
) -> bool {
475+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
476+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
477+
match record.r#type {
478+
PAYER_METADATA_TYPE => false, // Should be outside range
479+
INVOICE_REQUEST_PAYER_ID_TYPE => !self.payer.0.derives_keys(),
480+
_ => true,
481+
}
482+
});
483+
let tlv_stream = offer_records.chain(invreq_records);
484+
signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx)
485+
}
486+
466487
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
467488
let payer = PayerTlvStreamRef {
468489
metadata: self.payer.0.as_bytes(),
@@ -640,7 +661,9 @@ mod tests {
640661
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
641662
use core::convert::TryFrom;
642663
use core::time::Duration;
664+
use crate::chain::keysinterface::KeyMaterial;
643665
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
666+
use crate::ln::inbound_payment::ExpandedKey;
644667
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
645668
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
646669
use crate::offers::offer::OfferTlvStreamRef;
@@ -726,6 +749,118 @@ mod tests {
726749
}
727750
}
728751

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

0 commit comments

Comments
 (0)