Skip to content

Commit 8bbb02b

Browse files
committed
Stateless verification of InvoiceRequest
Verify that an InvoiceRequest was produced from an Offer constructed by the recipient using the Offer metadata reflected in the InvoiceRequest. The Offer metadata consists of a 128-bit nonce and a 256-bit HMAC over the nonce and Offer TLV records (excluding the signing pubkey). 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 282e7b9 commit 8bbb02b

File tree

3 files changed

+141
-5
lines changed

3 files changed

+141
-5
lines changed

lightning/src/offers/invoice_request.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ use core::convert::TryFrom;
6060
use crate::io;
6161
use crate::ln::PaymentHash;
6262
use crate::ln::features::InvoiceRequestFeatures;
63+
use crate::ln::inbound_payment::ExpandedKey;
6364
use crate::ln::msgs::DecodeError;
6465
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
65-
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
66+
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
6667
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
6768
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
6869
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
@@ -370,6 +371,12 @@ impl InvoiceRequest {
370371
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
371372
}
372373

374+
/// Verifies that the request was for an offer created using the given key.
375+
#[allow(unused)]
376+
pub(crate) fn verify(&self, key: &ExpandedKey) -> bool {
377+
self.contents.offer.verify(TlvStream::new(&self.bytes), key)
378+
}
379+
373380
#[cfg(test)]
374381
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
375382
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =

lightning/src/offers/offer.rs

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ use crate::ln::features::OfferFeatures;
7878
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
7979
use crate::ln::msgs::MAX_VALUE_MSAT;
8080
use crate::offers::invoice_request::InvoiceRequestBuilder;
81+
use crate::offers::merkle::TlvStream;
8182
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
82-
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
83+
use crate::offers::signer::{MetadataMaterial, DerivedPubkey, self};
8384
use crate::onion_message::BlindedPath;
8485
use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
8586
use crate::util::string::PrintableString;
@@ -543,6 +544,24 @@ impl OfferContents {
543544
self.signing_pubkey
544545
}
545546

547+
/// Verifies that the offer metadata was produced from the offer in the TLV stream.
548+
pub(super) fn verify(&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey) -> bool {
549+
match &self.metadata {
550+
Some(metadata) => {
551+
let tlv_stream = tlv_stream.range(OFFER_TYPES).filter(|record| {
552+
match record.r#type {
553+
// TODO: Assert value bytes == metadata?
554+
OFFER_METADATA_TYPE => false,
555+
OFFER_NODE_ID_TYPE => false,
556+
_ => true,
557+
}
558+
});
559+
signer::verify_metadata(metadata, key, tlv_stream)
560+
},
561+
None => false,
562+
}
563+
}
564+
546565
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
547566
let (currency, amount) = match &self.amount {
548567
None => (None, None),
@@ -630,9 +649,18 @@ impl Quantity {
630649
}
631650
}
632651

633-
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
652+
/// Valid type range for offer TLV records.
653+
const OFFER_TYPES: core::ops::Range<u64> = 1..80;
654+
655+
/// TLV record type for [`Offer::metadata`].
656+
const OFFER_METADATA_TYPE: u64 = 4;
657+
658+
/// TLV record type for [`Offer::signing_pubkey`].
659+
const OFFER_NODE_ID_TYPE: u64 = 22;
660+
661+
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
634662
(2, chains: (Vec<ChainHash>, WithoutLength)),
635-
(4, metadata: (Vec<u8>, WithoutLength)),
663+
(OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
636664
(6, currency: CurrencyCode),
637665
(8, amount: (u64, HighZeroBytesDroppedBigSize)),
638666
(10, description: (String, WithoutLength)),
@@ -641,7 +669,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
641669
(16, paths: (Vec<BlindedPath>, WithoutLength)),
642670
(18, issuer: (String, WithoutLength)),
643671
(20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
644-
(22, node_id: PublicKey),
672+
(OFFER_NODE_ID_TYPE, node_id: PublicKey),
645673
});
646674

647675
impl Bech32Encode for Offer {
@@ -729,9 +757,12 @@ mod tests {
729757
use core::convert::TryFrom;
730758
use core::num::NonZeroU64;
731759
use core::time::Duration;
760+
use crate::chain::keysinterface::KeyMaterial;
732761
use crate::ln::features::OfferFeatures;
762+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
733763
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
734764
use crate::offers::parse::{ParseError, SemanticError};
765+
use crate::offers::signer::DerivedPubkey;
735766
use crate::offers::test_utils::*;
736767
use crate::onion_message::{BlindedHop, BlindedPath};
737768
use crate::util::ser::{BigSize, Writeable};
@@ -840,6 +871,82 @@ mod tests {
840871
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
841872
}
842873

874+
#[test]
875+
fn builds_offer_with_metadata_derived() {
876+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
877+
let nonce = Nonce([42; Nonce::LENGTH]);
878+
879+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
880+
.amount_msats(1000)
881+
.metadata_derived(&expanded_key, nonce).unwrap()
882+
.build().unwrap();
883+
assert_eq!(offer.metadata().unwrap()[..Nonce::LENGTH], nonce.0);
884+
assert_eq!(offer.signing_pubkey(), recipient_pubkey());
885+
886+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
887+
.build().unwrap()
888+
.sign(payer_sign).unwrap();
889+
assert!(invoice_request.verify(&expanded_key));
890+
891+
let mut tlv_stream = offer.as_tlv_stream();
892+
tlv_stream.amount = Some(100);
893+
894+
let mut encoded_offer = Vec::new();
895+
tlv_stream.write(&mut encoded_offer).unwrap();
896+
897+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
898+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
899+
.build().unwrap()
900+
.sign(payer_sign).unwrap();
901+
assert!(!invoice_request.verify(&expanded_key));
902+
903+
match OfferBuilder::new("foo".into(), recipient_pubkey())
904+
.metadata_derived(&expanded_key, nonce).unwrap()
905+
.metadata_derived(&expanded_key, nonce)
906+
{
907+
Ok(_) => panic!("expected error"),
908+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
909+
}
910+
}
911+
912+
#[test]
913+
fn builds_offer_with_derived_signing_pubkey() {
914+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
915+
let nonce = Nonce([42; Nonce::LENGTH]);
916+
917+
let recipient_pubkey = DerivedPubkey::new(&expanded_key, nonce);
918+
let offer = OfferBuilder::deriving_signing_pubkey("foo".into(), recipient_pubkey)
919+
.amount_msats(1000)
920+
.build().unwrap();
921+
assert_eq!(offer.metadata().unwrap()[..Nonce::LENGTH], nonce.0);
922+
assert_eq!(offer.signing_pubkey(), expanded_key.signing_pubkey_for_offer(nonce));
923+
924+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
925+
.build().unwrap()
926+
.sign(payer_sign).unwrap();
927+
assert!(invoice_request.verify(&expanded_key));
928+
929+
let mut tlv_stream = offer.as_tlv_stream();
930+
tlv_stream.amount = Some(100);
931+
932+
let mut encoded_offer = Vec::new();
933+
tlv_stream.write(&mut encoded_offer).unwrap();
934+
935+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
936+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
937+
.build().unwrap()
938+
.sign(payer_sign).unwrap();
939+
assert!(!invoice_request.verify(&expanded_key));
940+
941+
let recipient_pubkey = DerivedPubkey::new(&expanded_key, nonce);
942+
match OfferBuilder::deriving_signing_pubkey("foo".into(), recipient_pubkey)
943+
.metadata_derived(&expanded_key, nonce)
944+
{
945+
Ok(_) => panic!("expected error"),
946+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
947+
}
948+
}
949+
843950
#[test]
844951
fn builds_offer_with_amount() {
845952
let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 };

lightning/src/offers/signer.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use core::convert::TryInto;
1616
use bitcoin::secp256k1::PublicKey;
1717
use crate::io;
1818
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
19+
use crate::offers::merkle::TlvRecord;
1920

2021
use crate::prelude::*;
2122

@@ -70,3 +71,24 @@ impl io::Write for MetadataMaterial {
7071
self.hmac.flush()
7172
}
7273
}
74+
75+
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
76+
/// - a 128-bit [`Nonce`] and
77+
/// - a [`Sha256Hash`] of the nonce and the TLV records using the [`ExpandedKey`].
78+
pub(super) fn verify_metadata<'a>(
79+
metadata: &Vec<u8>, expanded_key: &ExpandedKey,
80+
tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>
81+
) -> bool {
82+
let mut hmac = if metadata.len() < Nonce::LENGTH {
83+
return false;
84+
} else {
85+
let nonce = Nonce(metadata[..Nonce::LENGTH].try_into().unwrap());
86+
expanded_key.hmac_for_offer(nonce)
87+
};
88+
89+
for record in tlv_stream {
90+
hmac.input(record.record_bytes);
91+
}
92+
93+
&metadata[Nonce::LENGTH..] == &Hmac::from_engine(hmac).into_inner()
94+
}

0 commit comments

Comments
 (0)