Skip to content

Commit 9be1490

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 nonce and a 256-bit HMAC over the nonce and InvoiceRequest TLV records (excluding the payer id). 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 070a6f3 commit 9be1490

File tree

7 files changed

+181
-17
lines changed

7 files changed

+181
-17
lines changed

lightning/src/ln/inbound_payment.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use bitcoin::hashes::{Hash, HashEngine};
1414
use bitcoin::hashes::cmp::fixed_time_eq;
1515
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
1616
use bitcoin::hashes::sha256::{Hash as Sha256, self};
17-
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
17+
use bitcoin::secp256k1::{KeyPair, Secp256k1};
1818
use crate::chain::keysinterface::{KeyMaterial, EntropySource};
1919
use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
2020
use crate::ln::msgs;
@@ -78,18 +78,18 @@ impl ExpandedKey {
7878
hmac
7979
}
8080

81-
/// Derives a pubkey using the given nonce for use as [`Offer::signing_pubkey`].
81+
/// Derives keys using the given nonce for use with [`Offer::signing_pubkey`].
8282
///
8383
/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
8484
#[allow(unused)]
85-
pub(crate) fn signing_pubkey_for_offer(&self, nonce: Nonce) -> PublicKey {
85+
pub(crate) fn signing_keypair_for_offer(&self, nonce: Nonce) -> KeyPair {
8686
let mut engine = sha256::Hash::engine();
8787
engine.input(&self.offers_base_key);
8888
engine.input(&nonce.0);
8989

90-
let hash = sha256::Hash::from_engine(engine);
9190
let secp_ctx = Secp256k1::new();
92-
SecretKey::from_slice(&hash).unwrap().public_key(&secp_ctx)
91+
let hash = sha256::Hash::from_engine(engine);
92+
KeyPair::from_seckey_slice(&secp_ctx, &hash).unwrap()
9393
}
9494
}
9595

lightning/src/offers/invoice.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -463,8 +464,14 @@ impl Invoice {
463464
self.signature
464465
}
465466

467+
/// Verifies that the invoice was for a request or refund created using the given key.
468+
#[allow(unused)]
469+
pub(crate) fn verify(&self, key: &ExpandedKey) -> bool {
470+
self.contents.verify(TlvStream::new(&self.bytes), key)
471+
}
472+
466473
#[cfg(test)]
467-
fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
474+
pub(super) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
468475
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
469476
self.contents.as_tlv_stream();
470477
let signature_tlv_stream = SignatureTlvStreamRef {
@@ -506,6 +513,15 @@ impl InvoiceContents {
506513
}
507514
}
508515

516+
fn verify(&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey) -> bool {
517+
match self {
518+
InvoiceContents::ForOffer { invoice_request, .. } => {
519+
invoice_request.verify(tlv_stream, key)
520+
},
521+
_ => todo!(),
522+
}
523+
}
524+
509525
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
510526
let (payer, offer, invoice_request) = match self {
511527
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),

lightning/src/offers/invoice_request.rs

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ use crate::ln::inbound_payment::{ExpandedKey, Nonce};
6464
use crate::ln::msgs::DecodeError;
6565
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
6666
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
67-
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
67+
use crate::offers::offer::{OFFER_TYPES, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
6868
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
69-
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
70-
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
69+
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef};
70+
use crate::offers::signer::{MetadataMaterial, DerivedPubkey, self};
7171
use crate::onion_message::BlindedPath;
7272
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
7373
use crate::util::string::PrintableString;
@@ -446,6 +446,20 @@ impl InvoiceRequestContents {
446446
self.chain.unwrap_or_else(|| self.offer.implied_chain())
447447
}
448448

449+
/// Verifies that the payer metadata was produced from the invoice request in the TLV stream.
450+
pub(super) fn verify(&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey) -> bool {
451+
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
452+
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
453+
match record.r#type {
454+
PAYER_METADATA_TYPE => false, // Should be outside range
455+
INVOICE_REQUEST_PAYER_ID_TYPE => false,
456+
_ => true,
457+
}
458+
});
459+
let tlv_stream = offer_records.chain(invreq_records);
460+
signer::verify_metadata(&self.payer.0, key, tlv_stream)
461+
}
462+
449463
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
450464
let payer = PayerTlvStreamRef {
451465
metadata: Some(&self.payer.0),
@@ -483,12 +497,20 @@ impl Writeable for InvoiceRequestContents {
483497
}
484498
}
485499

486-
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
500+
/// Valid type range for invoice_request TLV records.
501+
const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
502+
503+
/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`].
504+
///
505+
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
506+
const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
507+
508+
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
487509
(80, chain: ChainHash),
488510
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
489511
(84, features: (InvoiceRequestFeatures, WithoutLength)),
490512
(86, quantity: (u64, HighZeroBytesDroppedBigSize)),
491-
(88, payer_id: PublicKey),
513+
(INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey),
492514
(89, payer_note: (String, WithoutLength)),
493515
});
494516

@@ -597,12 +619,16 @@ mod tests {
597619
use core::num::NonZeroU64;
598620
#[cfg(feature = "std")]
599621
use core::time::Duration;
622+
use crate::chain::keysinterface::KeyMaterial;
600623
use crate::ln::features::InvoiceRequestFeatures;
624+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
601625
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
626+
use crate::offers::invoice::{Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG};
602627
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
603628
use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity};
604629
use crate::offers::parse::{ParseError, SemanticError};
605630
use crate::offers::payer::PayerTlvStreamRef;
631+
use crate::offers::signer::DerivedPubkey;
606632
use crate::offers::test_utils::*;
607633
use crate::util::ser::{BigSize, Writeable};
608634
use crate::util::string::PrintableString;
@@ -695,6 +721,120 @@ mod tests {
695721
}
696722
}
697723

724+
#[test]
725+
fn builds_invoice_request_with_metadata_derived() {
726+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
727+
let nonce = Nonce([42; Nonce::LENGTH]);
728+
729+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
730+
.amount_msats(1000)
731+
.build().unwrap();
732+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
733+
.metadata_derived(&expanded_key, nonce).unwrap()
734+
.build().unwrap()
735+
.sign(payer_sign).unwrap();
736+
assert_eq!(invoice_request.metadata()[..Nonce::LENGTH], nonce.0);
737+
assert_eq!(invoice_request.payer_id(), payer_pubkey());
738+
739+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
740+
.unwrap()
741+
.build().unwrap()
742+
.sign(recipient_sign).unwrap();
743+
assert!(invoice.verify(&expanded_key));
744+
745+
let (
746+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
747+
mut invoice_tlv_stream, mut signature_tlv_stream
748+
) = invoice.as_tlv_stream();
749+
invoice_request_tlv_stream.amount = Some(2000);
750+
invoice_tlv_stream.amount = Some(2000);
751+
752+
let tlv_stream =
753+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
754+
let mut bytes = Vec::new();
755+
tlv_stream.write(&mut bytes).unwrap();
756+
757+
let signature = merkle::sign_message(
758+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
759+
).unwrap();
760+
signature_tlv_stream.signature = Some(&signature);
761+
762+
let mut encoded_invoice = bytes;
763+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
764+
765+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
766+
assert!(!invoice.verify(&expanded_key));
767+
768+
match OfferBuilder::new("foo".into(), recipient_pubkey())
769+
.build().unwrap()
770+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
771+
.metadata_derived(&expanded_key, nonce).unwrap()
772+
.metadata_derived(&expanded_key, nonce)
773+
{
774+
Ok(_) => panic!("expected error"),
775+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
776+
}
777+
}
778+
779+
#[test]
780+
fn builds_invoice_request_with_derived_payer_id() {
781+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
782+
let nonce = Nonce([42; Nonce::LENGTH]);
783+
let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
784+
785+
let secp_ctx = Secp256k1::new();
786+
let keys = expanded_key.signing_keypair_for_offer(nonce);
787+
788+
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
789+
.amount_msats(1000)
790+
.build().unwrap();
791+
let invoice_request = offer.request_invoice_deriving_payer_id(payer_pubkey).unwrap()
792+
.build().unwrap()
793+
.sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
794+
.unwrap();
795+
assert_eq!(invoice_request.metadata()[..Nonce::LENGTH], nonce.0);
796+
assert_eq!(invoice_request.payer_id(), keys.public_key());
797+
798+
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
799+
.unwrap()
800+
.build().unwrap()
801+
.sign(recipient_sign).unwrap();
802+
assert!(invoice.verify(&expanded_key));
803+
804+
let (
805+
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
806+
mut invoice_tlv_stream, mut signature_tlv_stream
807+
) = invoice.as_tlv_stream();
808+
invoice_request_tlv_stream.amount = Some(2000);
809+
invoice_tlv_stream.amount = Some(2000);
810+
811+
let tlv_stream =
812+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
813+
let mut bytes = Vec::new();
814+
tlv_stream.write(&mut bytes).unwrap();
815+
816+
let signature = merkle::sign_message(
817+
recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey()
818+
).unwrap();
819+
signature_tlv_stream.signature = Some(&signature);
820+
821+
let mut encoded_invoice = bytes;
822+
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
823+
824+
let invoice = Invoice::try_from(encoded_invoice).unwrap();
825+
assert!(!invoice.verify(&expanded_key));
826+
827+
let payer_pubkey = DerivedPubkey::new(&expanded_key, nonce);
828+
match OfferBuilder::new("foo".into(), recipient_pubkey())
829+
.build().unwrap()
830+
.request_invoice_deriving_payer_id(payer_pubkey).unwrap()
831+
.metadata_derived(&expanded_key, nonce)
832+
{
833+
Ok(_) => panic!("expected error"),
834+
Err(e) => assert_eq!(e, SemanticError::UnexpectedMetadata),
835+
}
836+
}
837+
698838
#[test]
699839
fn builds_invoice_request_with_chain() {
700840
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ impl Quantity {
669669
}
670670

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

674674
/// TLV record type for [`Offer::metadata`].
675675
const OFFER_METADATA_TYPE: u64 = 4;
@@ -932,13 +932,14 @@ mod tests {
932932
fn builds_offer_with_derived_signing_pubkey() {
933933
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
934934
let nonce = Nonce([42; Nonce::LENGTH]);
935+
let keys = expanded_key.signing_keypair_for_offer(nonce);
935936

936937
let recipient_pubkey = DerivedPubkey::new(&expanded_key, nonce);
937938
let offer = OfferBuilder::deriving_signing_pubkey("foo".into(), recipient_pubkey)
938939
.amount_msats(1000)
939940
.build().unwrap();
940941
assert_eq!(offer.metadata().unwrap()[..Nonce::LENGTH], nonce.0);
941-
assert_eq!(offer.signing_pubkey(), expanded_key.signing_pubkey_for_offer(nonce));
942+
assert_eq!(offer.signing_pubkey(), keys.public_key());
942943

943944
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
944945
.build().unwrap()

lightning/src/offers/payer.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ use crate::prelude::*;
2020
#[derive(Clone, Debug, PartialEq)]
2121
pub(super) struct PayerContents(pub Vec<u8>);
2222

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

lightning/src/offers/signer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub(crate) struct DerivedPubkey {
2828
impl DerivedPubkey {
2929
pub(crate) fn new(expanded_key: &ExpandedKey, nonce: Nonce) -> Self {
3030
Self {
31-
public_key: expanded_key.signing_pubkey_for_offer(nonce),
31+
public_key: expanded_key.signing_keypair_for_offer(nonce).public_key(),
3232
metadata_material: MetadataMaterial::new(nonce, expanded_key),
3333
}
3434
}

0 commit comments

Comments
 (0)