Skip to content

Commit 658b05f

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 encrypted nonce and possibly a 256-bit HMAC over the nonce and Offer TLV records (excluding the signing pubkey) using an ExpandedKey. Thus, the HMAC can be reproduced from the offer 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 signing pubkey.
1 parent 392d2bc commit 658b05f

File tree

5 files changed

+225
-14
lines changed

5 files changed

+225
-14
lines changed

lightning/src/ln/inbound_payment.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::util::crypto::hkdf_extract_expand_4x;
2323
use crate::util::errors::APIError;
2424
use crate::util::logger::Logger;
2525

26-
use core::convert::TryInto;
26+
use core::convert::{TryFrom, TryInto};
2727
use core::ops::Deref;
2828

2929
pub(crate) const IV_LEN: usize = 16;
@@ -89,8 +89,8 @@ impl ExpandedKey {
8989
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
9090
/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
9191
#[allow(unused)]
92-
#[derive(Clone, Copy)]
93-
pub(crate) struct Nonce([u8; Self::LENGTH]);
92+
#[derive(Clone, Copy, Debug, PartialEq)]
93+
pub(crate) struct Nonce(pub(crate) [u8; Self::LENGTH]);
9494

9595
impl Nonce {
9696
/// Number of bytes in the nonce.
@@ -114,6 +114,21 @@ impl Nonce {
114114
}
115115
}
116116

117+
impl TryFrom<&[u8]> for Nonce {
118+
type Error = ();
119+
120+
fn try_from(bytes: &[u8]) -> Result<Self, ()> {
121+
if bytes.len() != Self::LENGTH {
122+
return Err(());
123+
}
124+
125+
let mut copied_bytes = [0u8; Self::LENGTH];
126+
copied_bytes.copy_from_slice(bytes);
127+
128+
Ok(Self(copied_bytes))
129+
}
130+
}
131+
117132
enum Method {
118133
LdkPaymentHash = 0,
119134
UserPaymentHash = 1,

lightning/src/offers/invoice_request.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,16 @@
5454
5555
use bitcoin::blockdata::constants::ChainHash;
5656
use bitcoin::network::constants::Network;
57-
use bitcoin::secp256k1::{Message, PublicKey};
57+
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
5858
use bitcoin::secp256k1::schnorr::Signature;
5959
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};
@@ -372,6 +373,13 @@ impl InvoiceRequest {
372373
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
373374
}
374375

376+
/// Verifies that the request was for an offer created using the given key.
377+
pub fn verify<T: secp256k1::Signing>(
378+
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
379+
) -> bool {
380+
self.contents.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx)
381+
}
382+
375383
#[cfg(test)]
376384
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
377385
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =

lightning/src/offers/offer.rs

Lines changed: 146 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ use crate::ln::features::OfferFeatures;
8080
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
8181
use crate::ln::msgs::MAX_VALUE_MSAT;
8282
use crate::offers::invoice_request::InvoiceRequestBuilder;
83+
use crate::offers::merkle::TlvStream;
8384
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
84-
use crate::offers::signer::{Metadata, MetadataMaterial};
85+
use crate::offers::signer::{Metadata, MetadataMaterial, self};
8586
use crate::onion_message::BlindedPath;
8687
use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
8788
use crate::util::string::PrintableString;
@@ -149,10 +150,11 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
149150
/// recipient privacy by using a different signing pubkey for each offer. Otherwise, the
150151
/// provided `node_id` is used for the signing pubkey.
151152
///
152-
/// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used to
153-
/// verify that an [`InvoiceRequest`] was produced for the offer given an [`ExpandedKey`].
153+
/// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used by
154+
/// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an
155+
/// [`ExpandedKey`].
154156
///
155-
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
157+
/// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify
156158
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
157159
pub fn deriving_signing_pubkey<ES: Deref>(
158160
description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
@@ -564,6 +566,27 @@ impl OfferContents {
564566
self.signing_pubkey
565567
}
566568

569+
/// Verifies that the offer metadata was produced from the offer in the TLV stream.
570+
pub(super) fn verify<T: secp256k1::Signing>(
571+
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
572+
) -> bool {
573+
match self.metadata() {
574+
Some(metadata) => {
575+
let tlv_stream = tlv_stream.range(OFFER_TYPES).filter(|record| {
576+
match record.r#type {
577+
OFFER_METADATA_TYPE => false,
578+
OFFER_NODE_ID_TYPE => false,
579+
_ => true,
580+
}
581+
});
582+
signer::verify_metadata(
583+
metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
584+
)
585+
},
586+
None => false,
587+
}
588+
}
589+
567590
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
568591
let (currency, amount) = match &self.amount {
569592
None => (None, None),
@@ -651,9 +674,18 @@ impl Quantity {
651674
}
652675
}
653676

654-
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
677+
/// Valid type range for offer TLV records.
678+
const OFFER_TYPES: core::ops::Range<u64> = 1..80;
679+
680+
/// TLV record type for [`Offer::metadata`].
681+
const OFFER_METADATA_TYPE: u64 = 4;
682+
683+
/// TLV record type for [`Offer::signing_pubkey`].
684+
const OFFER_NODE_ID_TYPE: u64 = 22;
685+
686+
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
655687
(2, chains: (Vec<ChainHash>, WithoutLength)),
656-
(4, metadata: (Vec<u8>, WithoutLength)),
688+
(OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
657689
(6, currency: CurrencyCode),
658690
(8, amount: (u64, HighZeroBytesDroppedBigSize)),
659691
(10, description: (String, WithoutLength)),
@@ -662,7 +694,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
662694
(16, paths: (Vec<BlindedPath>, WithoutLength)),
663695
(18, issuer: (String, WithoutLength)),
664696
(20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
665-
(22, node_id: PublicKey),
697+
(OFFER_NODE_ID_TYPE, node_id: PublicKey),
666698
});
667699

668700
impl Bech32Encode for Offer {
@@ -749,10 +781,13 @@ mod tests {
749781

750782
use bitcoin::blockdata::constants::ChainHash;
751783
use bitcoin::network::constants::Network;
784+
use bitcoin::secp256k1::Secp256k1;
752785
use core::convert::TryFrom;
753786
use core::num::NonZeroU64;
754787
use core::time::Duration;
788+
use crate::chain::keysinterface::KeyMaterial;
755789
use crate::ln::features::OfferFeatures;
790+
use crate::ln::inbound_payment::ExpandedKey;
756791
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
757792
use crate::offers::parse::{ParseError, SemanticError};
758793
use crate::offers::test_utils::*;
@@ -863,6 +898,110 @@ mod tests {
863898
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
864899
}
865900

901+
#[test]
902+
fn builds_offer_with_metadata_derived() {
903+
let desc = "foo".to_string();
904+
let node_id = recipient_pubkey();
905+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
906+
let entropy = FixedEntropy {};
907+
let secp_ctx = Secp256k1::new();
908+
909+
let offer = OfferBuilder
910+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
911+
.amount_msats(1000)
912+
.build().unwrap();
913+
assert_eq!(offer.signing_pubkey(), node_id);
914+
915+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
916+
.build().unwrap()
917+
.sign(payer_sign).unwrap();
918+
assert!(invoice_request.verify(&expanded_key, &secp_ctx));
919+
920+
// Fails verification with altered offer field
921+
let mut tlv_stream = offer.as_tlv_stream();
922+
tlv_stream.amount = Some(100);
923+
924+
let mut encoded_offer = Vec::new();
925+
tlv_stream.write(&mut encoded_offer).unwrap();
926+
927+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
928+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
929+
.build().unwrap()
930+
.sign(payer_sign).unwrap();
931+
assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
932+
933+
// Fails verification with altered metadata
934+
let mut tlv_stream = offer.as_tlv_stream();
935+
let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect();
936+
tlv_stream.metadata = Some(&metadata);
937+
938+
let mut encoded_offer = Vec::new();
939+
tlv_stream.write(&mut encoded_offer).unwrap();
940+
941+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
942+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
943+
.build().unwrap()
944+
.sign(payer_sign).unwrap();
945+
assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
946+
}
947+
948+
#[test]
949+
fn builds_offer_with_derived_signing_pubkey() {
950+
let desc = "foo".to_string();
951+
let node_id = recipient_pubkey();
952+
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
953+
let entropy = FixedEntropy {};
954+
let secp_ctx = Secp256k1::new();
955+
956+
let blinded_path = BlindedPath {
957+
introduction_node_id: pubkey(40),
958+
blinding_point: pubkey(41),
959+
blinded_hops: vec![
960+
BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
961+
BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
962+
],
963+
};
964+
965+
let offer = OfferBuilder
966+
::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
967+
.amount_msats(1000)
968+
.path(blinded_path)
969+
.build().unwrap();
970+
assert_ne!(offer.signing_pubkey(), node_id);
971+
972+
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
973+
.build().unwrap()
974+
.sign(payer_sign).unwrap();
975+
assert!(invoice_request.verify(&expanded_key, &secp_ctx));
976+
977+
// Fails verification with altered offer field
978+
let mut tlv_stream = offer.as_tlv_stream();
979+
tlv_stream.amount = Some(100);
980+
981+
let mut encoded_offer = Vec::new();
982+
tlv_stream.write(&mut encoded_offer).unwrap();
983+
984+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
985+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
986+
.build().unwrap()
987+
.sign(payer_sign).unwrap();
988+
assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
989+
990+
// Fails verification with altered signing pubkey
991+
let mut tlv_stream = offer.as_tlv_stream();
992+
let signing_pubkey = pubkey(1);
993+
tlv_stream.node_id = Some(&signing_pubkey);
994+
995+
let mut encoded_offer = Vec::new();
996+
tlv_stream.write(&mut encoded_offer).unwrap();
997+
998+
let invoice_request = Offer::try_from(encoded_offer).unwrap()
999+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1000+
.build().unwrap()
1001+
.sign(payer_sign).unwrap();
1002+
assert!(!invoice_request.verify(&expanded_key, &secp_ctx));
1003+
}
1004+
8661005
#[test]
8671006
fn builds_offer_with_amount() {
8681007
let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 };

lightning/src/offers/signer.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
//! Utilities for signing offer messages and verifying metadata.
1111
1212
use bitcoin::hashes::{Hash, HashEngine};
13+
use bitcoin::hashes::cmp::fixed_time_eq;
1314
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
1415
use bitcoin::hashes::sha256::Hash as Sha256;
15-
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey, self};
16-
use core::convert::TryInto;
16+
use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self};
17+
use core::convert::TryFrom;
1718
use core::fmt;
1819
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
20+
use crate::offers::merkle::TlvRecord;
1921
use crate::util::ser::Writeable;
2022

2123
use crate::prelude::*;
@@ -140,3 +142,41 @@ impl MetadataMaterial {
140142
(self.nonce.as_slice().to_vec(), keys)
141143
}
142144
}
145+
146+
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
147+
/// - a 128-bit [`Nonce`] and possibly
148+
/// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`].
149+
///
150+
/// If the latter is not included in the metadata, the TLV stream is used to check if the given
151+
/// `signing_pubkey` can be derived from it.
152+
pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
153+
metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
154+
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
155+
secp_ctx: &Secp256k1<T>
156+
) -> bool {
157+
if metadata.len() < Nonce::LENGTH {
158+
return false;
159+
}
160+
161+
let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) {
162+
Ok(nonce) => nonce,
163+
Err(_) => return false,
164+
};
165+
let mut hmac = expanded_key.hmac_for_offer(nonce, iv_bytes);
166+
167+
for record in tlv_stream {
168+
hmac.input(record.record_bytes);
169+
}
170+
171+
if metadata.len() == Nonce::LENGTH {
172+
hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT);
173+
let hmac = Hmac::from_engine(hmac);
174+
let derived_pubkey = SecretKey::from_slice(hmac.as_inner()).unwrap().public_key(&secp_ctx);
175+
fixed_time_eq(&signing_pubkey.serialize(), &derived_pubkey.serialize())
176+
} else if metadata[Nonce::LENGTH..].len() == Sha256::LEN {
177+
hmac.input(DERIVED_METADATA_HMAC_INPUT);
178+
fixed_time_eq(&metadata[Nonce::LENGTH..], &Hmac::from_engine(hmac).into_inner())
179+
} else {
180+
false
181+
}
182+
}

lightning/src/offers/test_utils.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey};
1313
use bitcoin::secp256k1::schnorr::Signature;
1414
use core::convert::Infallible;
1515
use core::time::Duration;
16+
use crate::chain::keysinterface::EntropySource;
1617
use crate::ln::PaymentHash;
1718
use crate::ln::features::BlindedHopFeatures;
1819
use crate::offers::invoice::BlindedPayInfo;
@@ -108,3 +109,11 @@ pub(super) fn now() -> Duration {
108109
.duration_since(std::time::SystemTime::UNIX_EPOCH)
109110
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH")
110111
}
112+
113+
pub(super) struct FixedEntropy;
114+
115+
impl EntropySource for FixedEntropy {
116+
fn get_secure_random_bytes(&self) -> [u8; 32] {
117+
[42; 32]
118+
}
119+
}

0 commit comments

Comments
 (0)