Skip to content

Commit efda3db

Browse files
committed
Stateless verification of Invoice for Offer
Verify that an Invoice was produced from an InvoiceRequest 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 InvoiceRequest TLV records (excluding the payer id) using an ExpandedKey. Thus, the HMAC can be reproduced from the invoice request 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 b78abe4 commit efda3db

File tree

6 files changed

+209
-14
lines changed

6 files changed

+209
-14
lines changed

lightning/src/offers/invoice.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ use bitcoin::blockdata::constants::ChainHash;
9797
use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
9898
use bitcoin::hashes::Hash;
9999
use bitcoin::network::constants::Network;
100-
use bitcoin::secp256k1::{Message, PublicKey};
100+
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
101101
use bitcoin::secp256k1::schnorr::Signature;
102102
use bitcoin::util::address::{Address, Payload, WitnessVersion};
103103
use bitcoin::util::schnorr::TweakedPublicKey;
@@ -106,9 +106,10 @@ use core::time::Duration;
106106
use crate::io;
107107
use crate::ln::PaymentHash;
108108
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
109+
use crate::ln::inbound_payment::ExpandedKey;
109110
use crate::ln::msgs::DecodeError;
110111
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
111-
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self};
112+
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self};
112113
use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
113114
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
114115
use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
@@ -123,7 +124,7 @@ use std::time::SystemTime;
123124

124125
const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
125126

126-
const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
127+
pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
127128

128129
/// Builds an [`Invoice`] from either:
129130
/// - an [`InvoiceRequest`] for the "offer to be paid" flow or
@@ -476,8 +477,15 @@ impl Invoice {
476477
merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
477478
}
478479

480+
/// Verifies that the invoice was for a request or refund created using the given key.
481+
pub fn verify<T: secp256k1::Signing>(
482+
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
483+
) -> bool {
484+
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
485+
}
486+
479487
#[cfg(test)]
480-
fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
488+
pub(super) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
481489
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
482490
self.contents.as_tlv_stream();
483491
let signature_tlv_stream = SignatureTlvStreamRef {
@@ -520,6 +528,17 @@ impl InvoiceContents {
520528
}
521529
}
522530

531+
fn verify<T: secp256k1::Signing>(
532+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
533+
) -> bool {
534+
match self {
535+
InvoiceContents::ForOffer { invoice_request, .. } => {
536+
invoice_request.verify(tlv_stream, key, secp_ctx)
537+
},
538+
_ => todo!(),
539+
}
540+
}
541+
523542
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
524543
let (payer, offer, invoice_request) = match self {
525544
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),

lightning/src/offers/invoice_request.rs

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
6666
use crate::ln::msgs::DecodeError;
6767
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
6868
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
69-
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
69+
use crate::offers::offer::{OFFER_TYPES, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
7070
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
71-
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
72-
use crate::offers::signer::{Metadata, MetadataMaterial};
71+
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
72+
use crate::offers::signer::{Metadata, MetadataMaterial, self};
7373
use crate::onion_message::BlindedPath;
7474
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
7575
use crate::util::string::PrintableString;
@@ -532,6 +532,22 @@ impl InvoiceRequestContents {
532532
self.inner.chain()
533533
}
534534

535+
/// Verifies that the payer metadata was produced from the invoice request in the TLV stream.
536+
pub(super) fn verify<T: secp256k1::Signing>(
537+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
538+
) -> bool {
539+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
540+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
541+
match record.r#type {
542+
PAYER_METADATA_TYPE => false, // Should be outside range
543+
INVOICE_REQUEST_PAYER_ID_TYPE => !self.inner.payer.0.derives_keys(),
544+
_ => true,
545+
}
546+
});
547+
let tlv_stream = offer_records.chain(invreq_records);
548+
signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx)
549+
}
550+
535551
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
536552
let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream();
537553
invoice_request.payer_id = Some(&self.payer_id);
@@ -585,12 +601,20 @@ impl Writeable for InvoiceRequestContents {
585601
}
586602
}
587603

588-
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
604+
/// Valid type range for invoice_request TLV records.
605+
const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
606+
607+
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
608+
///
609+
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
610+
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
611+
612+
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
589613
(80, chain: ChainHash),
590614
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
591615
(84, features: (InvoiceRequestFeatures, WithoutLength)),
592616
(86, quantity: (u64, HighZeroBytesDroppedBigSize)),
593-
(88, payer_id: PublicKey),
617+
(INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey),
594618
(89, payer_note: (String, WithoutLength)),
595619
});
596620

@@ -702,8 +726,11 @@ mod tests {
702726
use core::num::NonZeroU64;
703727
#[cfg(feature = "std")]
704728
use core::time::Duration;
729+
use crate::chain::keysinterface::KeyMaterial;
705730
use crate::ln::features::InvoiceRequestFeatures;
731+
use crate::ln::inbound_payment::ExpandedKey;
706732
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
733+
use crate::offers::invoice::{Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG};
707734
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
708735
use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
709736
use crate::offers::parse::{ParseError, SemanticError};
@@ -800,6 +827,148 @@ mod tests {
800827
}
801828
}
802829

830+
#[test]
831+
fn builds_invoice_request_with_derived_metadata() {
832+
let payer_id = payer_pubkey();
833+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
834+
let entropy = FixedEntropy {};
835+
let secp_ctx = Secp256k1::new();
836+
837+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
838+
.amount_msats(1000)
839+
.build().unwrap();
840+
let invoice_request = offer
841+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
842+
.unwrap()
843+
.build().unwrap()
844+
.sign(payer_sign).unwrap();
845+
assert_eq!(invoice_request.payer_id(), payer_pubkey());
846+
847+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
848+
.unwrap()
849+
.build().unwrap()
850+
.sign(recipient_sign).unwrap();
851+
assert!(invoice.verify(&expanded_key, &secp_ctx));
852+
853+
// Fails verification with altered fields
854+
let (
855+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
856+
mut invoice_tlv_stream, mut signature_tlv_stream
857+
) = invoice.as_tlv_stream();
858+
invoice_request_tlv_stream.amount = Some(2000);
859+
invoice_tlv_stream.amount = Some(2000);
860+
861+
let tlv_stream =
862+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
863+
let mut bytes = Vec::new();
864+
tlv_stream.write(&mut bytes).unwrap();
865+
866+
let signature = merkle::sign_message(
867+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
868+
).unwrap();
869+
signature_tlv_stream.signature = Some(&signature);
870+
871+
let mut encoded_invoice = bytes;
872+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
873+
874+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
875+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
876+
877+
// Fails verification with altered metadata
878+
let (
879+
mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
880+
mut signature_tlv_stream
881+
) = invoice.as_tlv_stream();
882+
let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect();
883+
payer_tlv_stream.metadata = Some(&metadata);
884+
885+
let tlv_stream =
886+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
887+
let mut bytes = Vec::new();
888+
tlv_stream.write(&mut bytes).unwrap();
889+
890+
let signature = merkle::sign_message(
891+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
892+
).unwrap();
893+
signature_tlv_stream.signature = Some(&signature);
894+
895+
let mut encoded_invoice = bytes;
896+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
897+
898+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
899+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
900+
}
901+
902+
#[test]
903+
fn builds_invoice_request_with_derived_payer_id() {
904+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
905+
let entropy = FixedEntropy {};
906+
let secp_ctx = Secp256k1::new();
907+
908+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
909+
.amount_msats(1000)
910+
.build().unwrap();
911+
let invoice_request = offer
912+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
913+
.unwrap()
914+
.build_and_sign()
915+
.unwrap();
916+
917+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
918+
.unwrap()
919+
.build().unwrap()
920+
.sign(recipient_sign).unwrap();
921+
assert!(invoice.verify(&expanded_key, &secp_ctx));
922+
923+
// Fails verification with altered fields
924+
let (
925+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
926+
mut invoice_tlv_stream, mut signature_tlv_stream
927+
) = invoice.as_tlv_stream();
928+
invoice_request_tlv_stream.amount = Some(2000);
929+
invoice_tlv_stream.amount = Some(2000);
930+
931+
let tlv_stream =
932+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
933+
let mut bytes = Vec::new();
934+
tlv_stream.write(&mut bytes).unwrap();
935+
936+
let signature = merkle::sign_message(
937+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
938+
).unwrap();
939+
signature_tlv_stream.signature = Some(&signature);
940+
941+
let mut encoded_invoice = bytes;
942+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
943+
944+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
945+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
946+
947+
// Fails verification with altered payer id
948+
let (
949+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream,
950+
mut signature_tlv_stream
951+
) = invoice.as_tlv_stream();
952+
let payer_id = pubkey(1);
953+
invoice_request_tlv_stream.payer_id = Some(&payer_id);
954+
955+
let tlv_stream =
956+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
957+
let mut bytes = Vec::new();
958+
tlv_stream.write(&mut bytes).unwrap();
959+
960+
let signature = merkle::sign_message(
961+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
962+
).unwrap();
963+
signature_tlv_stream.signature = Some(&signature);
964+
965+
let mut encoded_invoice = bytes;
966+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
967+
968+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
969+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
970+
}
971+
803972
#[test]
804973
fn builds_invoice_request_with_chain() {
805974
let mainnet = ChainHash::using_genesis_block(Network::Bitcoin);

lightning/src/offers/merkle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ fn tagged_branch_hash_from_engine(
143143

144144
/// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a
145145
/// well-formed TLV stream.
146+
#[derive(Clone)]
146147
pub(super) struct TlvStream<'a> {
147148
data: io::Cursor<&'a [u8]>,
148149
}

lightning/src/offers/offer.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,8 @@ impl Offer {
443443
/// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
444444
/// request, and
445445
/// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
446-
/// that it can be used to determine if the invoice was requested using a base [`ExpandedKey`]
447-
/// from which the payer id was derived.
446+
/// that it can be used by [`Invoice::verify`] to determine if the invoice was requested using
447+
/// a base [`ExpandedKey`] from which the payer id was derived.
448448
///
449449
/// Useful to protect the sender's privacy.
450450
///
@@ -722,7 +722,7 @@ impl Quantity {
722722
}
723723

724724
/// Valid type range for offer TLV records.
725-
const OFFER_TYPES: core::ops::Range<u64> = 1..80;
725+
pub(super) const OFFER_TYPES: core::ops::Range<u64> = 1..80;
726726

727727
/// TLV record type for [`Offer::metadata`].
728728
const OFFER_METADATA_TYPE: u64 = 4;

lightning/src/offers/payer.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ use crate::prelude::*;
2222
#[cfg_attr(test, derive(PartialEq))]
2323
pub(super) struct PayerContents(pub Metadata);
2424

25+
/// TLV record type for [`InvoiceRequest::metadata`] and [`Refund::metadata`].
26+
///
27+
/// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata
28+
/// [`Refund::metadata`]: crate::offers::refund::Refund::metadata
29+
pub(super) const PAYER_METADATA_TYPE: u64 = 0;
30+
2531
tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
26-
(0, metadata: (Vec<u8>, WithoutLength)),
32+
(PAYER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
2733
});

lightning/src/offers/signer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ impl MetadataMaterial {
164164
/// If the latter is not included in the metadata, the TLV stream is used to check if the given
165165
/// `signing_pubkey` can be derived from it.
166166
pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
167-
metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
167+
metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
168168
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
169169
secp_ctx: &Secp256k1<T>
170170
) -> bool {

0 commit comments

Comments
 (0)