Skip to content

Commit c960112

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 95a43ee commit c960112

File tree

5 files changed

+226
-15
lines changed

5 files changed

+226
-15
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,
@@ -566,6 +568,27 @@ impl OfferContents {
566568
self.signing_pubkey
567569
}
568570

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

656-
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
679+
/// Valid type range for offer TLV records.
680+
const OFFER_TYPES: core::ops::Range<u64> = 1..80;
681+
682+
/// TLV record type for [`Offer::metadata`].
683+
const OFFER_METADATA_TYPE: u64 = 4;
684+
685+
/// TLV record type for [`Offer::signing_pubkey`].
686+
const OFFER_NODE_ID_TYPE: u64 = 22;
687+
688+
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
657689
(2, chains: (Vec<ChainHash>, WithoutLength)),
658-
(4, metadata: (Vec<u8>, WithoutLength)),
690+
(OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
659691
(6, currency: CurrencyCode),
660692
(8, amount: (u64, HighZeroBytesDroppedBigSize)),
661693
(10, description: (String, WithoutLength)),
@@ -664,7 +696,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
664696
(16, paths: (Vec<BlindedPath>, WithoutLength)),
665697
(18, issuer: (String, WithoutLength)),
666698
(20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
667-
(22, node_id: PublicKey),
699+
(OFFER_NODE_ID_TYPE, node_id: PublicKey),
668700
});
669701

670702
impl Bech32Encode for Offer {
@@ -751,10 +783,13 @@ mod tests {
751783

752784
use bitcoin::blockdata::constants::ChainHash;
753785
use bitcoin::network::constants::Network;
786+
use bitcoin::secp256k1::Secp256k1;
754787
use core::convert::TryFrom;
755788
use core::num::NonZeroU64;
756789
use core::time::Duration;
790+
use crate::chain::keysinterface::KeyMaterial;
757791
use crate::ln::features::OfferFeatures;
792+
use crate::ln::inbound_payment::ExpandedKey;
758793
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
759794
use crate::offers::parse::{ParseError, SemanticError};
760795
use crate::offers::test_utils::*;
@@ -865,6 +900,110 @@ mod tests {
865900
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
866901
}
867902

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

lightning/src/offers/signer.rs

Lines changed: 43 additions & 3 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::*;
@@ -56,7 +58,7 @@ impl Metadata {
5658

5759
pub fn derives_keys(&self) -> bool {
5860
match self {
59-
Metadata::Bytes(_) => false,
61+
Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH,
6062
Metadata::Derived(_) => false,
6163
Metadata::DerivedSigningPubkey(_) => true,
6264
}
@@ -148,3 +150,41 @@ impl MetadataMaterial {
148150
(self.nonce.as_slice().to_vec(), keys)
149151
}
150152
}
153+
154+
/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of:
155+
/// - a 128-bit [`Nonce`] and possibly
156+
/// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`].
157+
///
158+
/// If the latter is not included in the metadata, the TLV stream is used to check if the given
159+
/// `signing_pubkey` can be derived from it.
160+
pub(super) fn verify_metadata<'a, T: secp256k1::Signing>(
161+
metadata: &Vec<u8>, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
162+
signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>,
163+
secp_ctx: &Secp256k1<T>
164+
) -> bool {
165+
if metadata.len() < Nonce::LENGTH {
166+
return false;
167+
}
168+
169+
let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) {
170+
Ok(nonce) => nonce,
171+
Err(_) => return false,
172+
};
173+
let mut hmac = expanded_key.hmac_for_offer(nonce, iv_bytes);
174+
175+
for record in tlv_stream {
176+
hmac.input(record.record_bytes);
177+
}
178+
179+
if metadata.len() == Nonce::LENGTH {
180+
hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT);
181+
let hmac = Hmac::from_engine(hmac);
182+
let derived_pubkey = SecretKey::from_slice(hmac.as_inner()).unwrap().public_key(&secp_ctx);
183+
fixed_time_eq(&signing_pubkey.serialize(), &derived_pubkey.serialize())
184+
} else if metadata[Nonce::LENGTH..].len() == Sha256::LEN {
185+
hmac.input(DERIVED_METADATA_HMAC_INPUT);
186+
fixed_time_eq(&metadata[Nonce::LENGTH..], &Hmac::from_engine(hmac).into_inner())
187+
} else {
188+
false
189+
}
190+
}

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)