Skip to content

Commit 94c7679

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 f8b378b commit 94c7679

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;
@@ -529,6 +529,22 @@ impl InvoiceRequestContents {
529529
self.inner.chain()
530530
}
531531

532+
/// Verifies that the payer metadata was produced from the invoice request in the TLV stream.
533+
pub(super) fn verify<T: secp256k1::Signing>(
534+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
535+
) -> bool {
536+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
537+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
538+
match record.r#type {
539+
PAYER_METADATA_TYPE => false, // Should be outside range
540+
INVOICE_REQUEST_PAYER_ID_TYPE => false,
541+
_ => true,
542+
}
543+
});
544+
let tlv_stream = offer_records.chain(invreq_records);
545+
signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx)
546+
}
547+
532548
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
533549
let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream();
534550
invoice_request.payer_id = Some(&self.payer_id);
@@ -582,12 +598,20 @@ impl Writeable for InvoiceRequestContents {
582598
}
583599
}
584600

585-
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
601+
/// Valid type range for invoice_request TLV records.
602+
const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
603+
604+
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
605+
///
606+
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
607+
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
608+
609+
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
586610
(80, chain: ChainHash),
587611
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
588612
(84, features: (InvoiceRequestFeatures, WithoutLength)),
589613
(86, quantity: (u64, HighZeroBytesDroppedBigSize)),
590-
(88, payer_id: PublicKey),
614+
(INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey),
591615
(89, payer_note: (String, WithoutLength)),
592616
});
593617

@@ -699,8 +723,11 @@ mod tests {
699723
use core::num::NonZeroU64;
700724
#[cfg(feature = "std")]
701725
use core::time::Duration;
726+
use crate::chain::keysinterface::KeyMaterial;
702727
use crate::ln::features::InvoiceRequestFeatures;
728+
use crate::ln::inbound_payment::ExpandedKey;
703729
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
730+
use crate::offers::invoice::{Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG};
704731
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
705732
use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
706733
use crate::offers::parse::{ParseError, SemanticError};
@@ -797,6 +824,148 @@ mod tests {
797824
}
798825
}
799826

827+
#[test]
828+
fn builds_invoice_request_with_derived_metadata() {
829+
let payer_id = payer_pubkey();
830+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
831+
let entropy = FixedEntropy {};
832+
let secp_ctx = Secp256k1::new();
833+
834+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
835+
.amount_msats(1000)
836+
.build().unwrap();
837+
let invoice_request = offer
838+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
839+
.unwrap()
840+
.build().unwrap()
841+
.sign(payer_sign).unwrap();
842+
assert_eq!(invoice_request.payer_id(), payer_pubkey());
843+
844+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
845+
.unwrap()
846+
.build().unwrap()
847+
.sign(recipient_sign).unwrap();
848+
assert!(invoice.verify(&expanded_key, &secp_ctx));
849+
850+
// Fails verification with altered fields
851+
let (
852+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
853+
mut invoice_tlv_stream, mut signature_tlv_stream
854+
) = invoice.as_tlv_stream();
855+
invoice_request_tlv_stream.amount = Some(2000);
856+
invoice_tlv_stream.amount = Some(2000);
857+
858+
let tlv_stream =
859+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
860+
let mut bytes = Vec::new();
861+
tlv_stream.write(&mut bytes).unwrap();
862+
863+
let signature = merkle::sign_message(
864+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
865+
).unwrap();
866+
signature_tlv_stream.signature = Some(&signature);
867+
868+
let mut encoded_invoice = bytes;
869+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
870+
871+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
872+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
873+
874+
// Fails verification with altered metadata
875+
let (
876+
mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
877+
mut signature_tlv_stream
878+
) = invoice.as_tlv_stream();
879+
let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect();
880+
payer_tlv_stream.metadata = Some(&metadata);
881+
882+
let tlv_stream =
883+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
884+
let mut bytes = Vec::new();
885+
tlv_stream.write(&mut bytes).unwrap();
886+
887+
let signature = merkle::sign_message(
888+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
889+
).unwrap();
890+
signature_tlv_stream.signature = Some(&signature);
891+
892+
let mut encoded_invoice = bytes;
893+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
894+
895+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
896+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
897+
}
898+
899+
#[test]
900+
fn builds_invoice_request_with_derived_payer_id() {
901+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
902+
let entropy = FixedEntropy {};
903+
let secp_ctx = Secp256k1::new();
904+
905+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
906+
.amount_msats(1000)
907+
.build().unwrap();
908+
let invoice_request = offer
909+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
910+
.unwrap()
911+
.build_and_sign()
912+
.unwrap();
913+
914+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
915+
.unwrap()
916+
.build().unwrap()
917+
.sign(recipient_sign).unwrap();
918+
assert!(invoice.verify(&expanded_key, &secp_ctx));
919+
920+
// Fails verification with altered fields
921+
let (
922+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
923+
mut invoice_tlv_stream, mut signature_tlv_stream
924+
) = invoice.as_tlv_stream();
925+
invoice_request_tlv_stream.amount = Some(2000);
926+
invoice_tlv_stream.amount = Some(2000);
927+
928+
let tlv_stream =
929+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
930+
let mut bytes = Vec::new();
931+
tlv_stream.write(&mut bytes).unwrap();
932+
933+
let signature = merkle::sign_message(
934+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
935+
).unwrap();
936+
signature_tlv_stream.signature = Some(&signature);
937+
938+
let mut encoded_invoice = bytes;
939+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
940+
941+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
942+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
943+
944+
// Fails verification with altered payer id
945+
let (
946+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream,
947+
mut signature_tlv_stream
948+
) = invoice.as_tlv_stream();
949+
let payer_id = pubkey(1);
950+
invoice_request_tlv_stream.payer_id = Some(&payer_id);
951+
952+
let tlv_stream =
953+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
954+
let mut bytes = Vec::new();
955+
tlv_stream.write(&mut bytes).unwrap();
956+
957+
let signature = merkle::sign_message(
958+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
959+
).unwrap();
960+
signature_tlv_stream.signature = Some(&signature);
961+
962+
let mut encoded_invoice = bytes;
963+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
964+
965+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
966+
assert!(!invoice.verify(&expanded_key, &secp_ctx));
967+
}
968+
800969
#[test]
801970
fn builds_invoice_request_with_chain() {
802971
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
@@ -441,8 +441,8 @@ impl Offer {
441441
/// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
442442
/// request, and
443443
/// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
444-
/// that it can be used to determine if the invoice was requested using a base [`ExpandedKey`]
445-
/// from which the payer id was derived.
444+
/// that it can be used by [`Invoice::verify`] to determine if the invoice was requested using
445+
/// a base [`ExpandedKey`] from which the payer id was derived.
446446
///
447447
/// Useful to protect the sender's privacy.
448448
///
@@ -720,7 +720,7 @@ impl Quantity {
720720
}
721721

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

725725
/// TLV record type for [`Offer::metadata`].
726726
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
@@ -156,7 +156,7 @@ impl MetadataMaterial {
156156
/// If the latter is not included in the metadata, the TLV stream is used to check if the given
157157
/// `signing_pubkey` can be derived from it.
158158
pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
159-
metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
159+
metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
160160
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
161161
secp_ctx: &Secp256k1<T>
162162
) -> bool {

0 commit comments

Comments
 (0)