Skip to content

Commit 6ba2ba7

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 48fdc62 commit 6ba2ba7

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;
@@ -542,6 +543,24 @@ impl OfferContents {
542543
self.signing_pubkey
543544
}
544545

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

632-
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
651+
/// Valid type range for offer TLV records.
652+
const OFFER_TYPES: core::ops::Range<u64> = 1..80;
653+
654+
/// TLV record type for [`Offer::metadata`].
655+
const OFFER_METADATA_TYPE: u64 = 4;
656+
657+
/// TLV record type for [`Offer::signing_pubkey`].
658+
const OFFER_NODE_ID_TYPE: u64 = 22;
659+
660+
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
633661
(2, chains: (Vec<ChainHash>, WithoutLength)),
634-
(4, metadata: (Vec<u8>, WithoutLength)),
662+
(OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
635663
(6, currency: CurrencyCode),
636664
(8, amount: (u64, HighZeroBytesDroppedBigSize)),
637665
(10, description: (String, WithoutLength)),
@@ -640,7 +668,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
640668
(16, paths: (Vec<BlindedPath>, WithoutLength)),
641669
(18, issuer: (String, WithoutLength)),
642670
(20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
643-
(22, node_id: PublicKey),
671+
(OFFER_NODE_ID_TYPE, node_id: PublicKey),
644672
});
645673

646674
impl Bech32Encode for Offer {
@@ -728,9 +756,12 @@ mod tests {
728756
use core::convert::TryFrom;
729757
use core::num::NonZeroU64;
730758
use core::time::Duration;
759+
use crate::chain::keysinterface::KeyMaterial;
731760
use crate::ln::features::OfferFeatures;
761+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
732762
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
733763
use crate::offers::parse::{ParseError, SemanticError};
764+
use crate::offers::signer::DerivedPubkey;
734765
use crate::offers::test_utils::*;
735766
use crate::onion_message::{BlindedHop, BlindedPath};
736767
use crate::util::ser::{BigSize, Writeable};
@@ -839,6 +870,82 @@ mod tests {
839870
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
840871
}
841872

873+
#[test]
874+
fn builds_offer_with_metadata_derived() {
875+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
876+
let nonce = Nonce([42; Nonce::LENGTH]);
877+
878+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
879+
.amount_msats(1000)
880+
.metadata_derived(&expanded_key, nonce).unwrap()
881+
.build().unwrap();
882+
assert_eq!(offer.metadata().unwrap()[..Nonce::LENGTH], nonce.0);
883+
assert_eq!(offer.signing_pubkey(), recipient_pubkey());
884+
885+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
886+
.build().unwrap()
887+
.sign(payer_sign).unwrap();
888+
assert!(invoice_request.verify(&expanded_key));
889+
890+
let mut tlv_stream = offer.as_tlv_stream();
891+
tlv_stream.amount = Some(100);
892+
893+
let mut encoded_offer = Vec::new();
894+
tlv_stream.write(&mut encoded_offer).unwrap();
895+
896+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
897+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
898+
.build().unwrap()
899+
.sign(payer_sign).unwrap();
900+
assert!(!invoice_request.verify(&expanded_key));
901+
902+
match OfferBuilder::new("foo".into(), recipient_pubkey())
903+
.metadata_derived(&expanded_key, nonce).unwrap()
904+
.metadata_derived(&expanded_key, nonce)
905+
{
906+
Ok(_) => panic!("expected error"),
907+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
908+
}
909+
}
910+
911+
#[test]
912+
fn builds_offer_with_derived_signing_pubkey() {
913+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
914+
let nonce = Nonce([42; Nonce::LENGTH]);
915+
916+
let recipient_pubkey = DerivedPubkey::new(&expanded_key, nonce);
917+
let offer = OfferBuilder::deriving_signing_pubkey("foo".into(), recipient_pubkey)
918+
.amount_msats(1000)
919+
.build().unwrap();
920+
assert_eq!(offer.metadata().unwrap()[..Nonce::LENGTH], nonce.0);
921+
assert_eq!(offer.signing_pubkey(), expanded_key.signing_pubkey_for_offer(nonce));
922+
923+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
924+
.build().unwrap()
925+
.sign(payer_sign).unwrap();
926+
assert!(invoice_request.verify(&expanded_key));
927+
928+
let mut tlv_stream = offer.as_tlv_stream();
929+
tlv_stream.amount = Some(100);
930+
931+
let mut encoded_offer = Vec::new();
932+
tlv_stream.write(&mut encoded_offer).unwrap();
933+
934+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
935+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
936+
.build().unwrap()
937+
.sign(payer_sign).unwrap();
938+
assert!(!invoice_request.verify(&expanded_key));
939+
940+
let recipient_pubkey = DerivedPubkey::new(&expanded_key, nonce);
941+
match OfferBuilder::deriving_signing_pubkey("foo".into(), recipient_pubkey)
942+
.metadata_derived(&expanded_key, nonce)
943+
{
944+
Ok(_) => panic!("expected error"),
945+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
946+
}
947+
}
948+
842949
#[test]
843950
fn builds_offer_with_amount() {
844951
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
/// A pubkey derived from a base key and nonce. Used to crate metadata for a message such that it
2122
/// can be verified using [`verify_metadata`].
@@ -68,3 +69,24 @@ impl io::Write for MetadataMaterial {
6869
self.hmac.flush()
6970
}
7071
}
72+
73+
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
74+
/// - a 128-bit [`Nonce`] and
75+
/// - a [`Sha256Hash`] of the nonce and the TLV records using the [`ExpandedKey`].
76+
pub(super) fn verify_metadata<'a>(
77+
metadata: &Vec<u8>, expanded_key: &ExpandedKey,
78+
tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>
79+
) -> bool {
80+
let mut hmac = if metadata.len() < Nonce::LENGTH {
81+
return false;
82+
} else {
83+
let nonce = Nonce(metadata[..Nonce::LENGTH].try_into().unwrap());
84+
expanded_key.hmac_for_offer(nonce)
85+
};
86+
87+
for record in tlv_stream {
88+
hmac.input(record.record_bytes);
89+
}
90+
91+
&metadata[Nonce::LENGTH..] == &Hmac::from_engine(hmac).into_inner()
92+
}

0 commit comments

Comments
 (0)