Skip to content

Commit 55f027b

Browse files
committed
Offer metadata and signing pubkey derivation
Add support for deriving a transient signing pubkey for each Offer from an ExpandedKey and a nonce. This facilitates recipient privacy by not tying any Offer to any other nor to the recipient's node id. Additionally, support stateless Offer verification by setting its metadata using an HMAC over the nonce and the remaining TLV records, which will be later verified when receiving an InvoiceRequest.
1 parent 336fc02 commit 55f027b

File tree

8 files changed

+322
-42
lines changed

8 files changed

+322
-42
lines changed

lightning/src/ln/inbound_payment.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::util::logger::Logger;
2626
use core::convert::TryInto;
2727
use core::ops::Deref;
2828

29-
const IV_LEN: usize = 16;
29+
pub(crate) const IV_LEN: usize = 16;
3030
const METADATA_LEN: usize = 16;
3131
const METADATA_KEY_LEN: usize = 32;
3232
const AMT_MSAT_LEN: usize = 8;
@@ -66,6 +66,52 @@ impl ExpandedKey {
6666
offers_base_key,
6767
}
6868
}
69+
70+
/// Returns an [`HmacEngine`] used to construct [`Offer::metadata`].
71+
///
72+
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
73+
#[allow(unused)]
74+
pub(crate) fn hmac_for_offer(
75+
&self, nonce: Nonce, iv_bytes: &[u8; IV_LEN]
76+
) -> HmacEngine<Sha256> {
77+
let mut hmac = HmacEngine::<Sha256>::new(&self.offers_base_key);
78+
hmac.input(iv_bytes);
79+
hmac.input(&nonce.0);
80+
hmac
81+
}
82+
}
83+
84+
/// A 128-bit number used only once.
85+
///
86+
/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from
87+
/// [`ExpandedKey`]. Must not be reused for any other derivation without first hashing.
88+
///
89+
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
90+
/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
91+
#[allow(unused)]
92+
#[derive(Clone, Copy)]
93+
pub(crate) struct Nonce([u8; Self::LENGTH]);
94+
95+
impl Nonce {
96+
/// Number of bytes in the nonce.
97+
pub const LENGTH: usize = 16;
98+
99+
/// Creates a `Nonce` from the given [`EntropySource`].
100+
pub fn from_entropy_source<ES: Deref>(entropy_source: ES) -> Self
101+
where
102+
ES::Target: EntropySource,
103+
{
104+
let mut bytes = [0u8; Self::LENGTH];
105+
let rand_bytes = entropy_source.get_secure_random_bytes();
106+
bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]);
107+
108+
Nonce(bytes)
109+
}
110+
111+
/// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`].
112+
pub fn as_slice(&self) -> &[u8] {
113+
&self.0
114+
}
69115
}
70116

71117
enum Method {

lightning/src/offers/invoice.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ impl<'a> UnsignedInvoice<'a> {
313313
/// [`Offer`]: crate::offers::offer::Offer
314314
/// [`Refund`]: crate::offers::refund::Refund
315315
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
316-
#[derive(Clone, Debug, PartialEq)]
316+
#[derive(Clone, Debug)]
317+
#[cfg_attr(test, derive(PartialEq))]
317318
pub struct Invoice {
318319
bytes: Vec<u8>,
319320
contents: InvoiceContents,
@@ -324,7 +325,8 @@ pub struct Invoice {
324325
///
325326
/// [`Offer`]: crate::offers::offer::Offer
326327
/// [`Refund`]: crate::offers::refund::Refund
327-
#[derive(Clone, Debug, PartialEq)]
328+
#[derive(Clone, Debug)]
329+
#[cfg_attr(test, derive(PartialEq))]
328330
enum InvoiceContents {
329331
/// Contents for an [`Invoice`] corresponding to an [`Offer`].
330332
///

lightning/src/offers/invoice_request.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ impl<'a> UnsignedInvoiceRequest<'a> {
250250
///
251251
/// [`Invoice`]: crate::offers::invoice::Invoice
252252
/// [`Offer`]: crate::offers::offer::Offer
253-
#[derive(Clone, Debug, PartialEq)]
253+
#[derive(Clone, Debug)]
254+
#[cfg_attr(test, derive(PartialEq))]
254255
pub struct InvoiceRequest {
255256
pub(super) bytes: Vec<u8>,
256257
pub(super) contents: InvoiceRequestContents,
@@ -260,7 +261,8 @@ pub struct InvoiceRequest {
260261
/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
261262
///
262263
/// [`Invoice`]: crate::offers::invoice::Invoice
263-
#[derive(Clone, Debug, PartialEq)]
264+
#[derive(Clone, Debug)]
265+
#[cfg_attr(test, derive(PartialEq))]
264266
pub(super) struct InvoiceRequestContents {
265267
payer: PayerContents,
266268
pub(super) offer: OfferContents,

lightning/src/offers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ pub mod offer;
1919
pub mod parse;
2020
mod payer;
2121
pub mod refund;
22+
#[allow(unused)]
23+
pub(crate) mod signer;
2224
#[cfg(test)]
2325
mod test_utils;

lightning/src/offers/offer.rs

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,20 @@
6868
6969
use bitcoin::blockdata::constants::ChainHash;
7070
use bitcoin::network::constants::Network;
71-
use bitcoin::secp256k1::PublicKey;
71+
use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
7272
use core::convert::TryFrom;
7373
use core::num::NonZeroU64;
74+
use core::ops::Deref;
7475
use core::str::FromStr;
7576
use core::time::Duration;
77+
use crate::chain::keysinterface::EntropySource;
7678
use crate::io;
7779
use crate::ln::features::OfferFeatures;
80+
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
7881
use crate::ln::msgs::MAX_VALUE_MSAT;
7982
use crate::offers::invoice_request::InvoiceRequestBuilder;
8083
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
84+
use crate::offers::signer::{Metadata, MetadataMaterial};
8185
use crate::onion_message::BlindedPath;
8286
use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
8387
use crate::util::string::PrintableString;
@@ -87,30 +91,89 @@ use crate::prelude::*;
8791
#[cfg(feature = "std")]
8892
use std::time::SystemTime;
8993

94+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~";
95+
9096
/// Builds an [`Offer`] for the "offer to be paid" flow.
9197
///
9298
/// See [module-level documentation] for usage.
9399
///
94100
/// [module-level documentation]: self
95-
pub struct OfferBuilder {
101+
pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> {
96102
offer: OfferContents,
103+
metadata_strategy: core::marker::PhantomData<M>,
104+
secp_ctx: Option<&'a Secp256k1<T>>,
97105
}
98106

99-
impl OfferBuilder {
107+
/// Indicates how [`Offer::metadata`] may be set.
108+
pub trait MetadataStrategy {}
109+
110+
/// [`Offer::metadata`] may be explicitly set or left empty.
111+
pub struct ExplicitMetadata {}
112+
113+
/// [`Offer::metadata`] will be derived.
114+
pub struct DerivedMetadata {}
115+
116+
impl MetadataStrategy for ExplicitMetadata {}
117+
impl MetadataStrategy for DerivedMetadata {}
118+
119+
impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> {
100120
/// Creates a new builder for an offer setting the [`Offer::description`] and using the
101121
/// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered
102122
/// while the offer is valid.
103123
///
104124
/// Use a different pubkey per offer to avoid correlating offers.
105125
pub fn new(description: String, signing_pubkey: PublicKey) -> Self {
106-
let offer = OfferContents {
107-
chains: None, metadata: None, amount: None, description,
108-
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
109-
supported_quantity: Quantity::One, signing_pubkey,
110-
};
111-
OfferBuilder { offer }
126+
OfferBuilder {
127+
offer: OfferContents {
128+
chains: None, metadata: None, amount: None, description,
129+
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
130+
supported_quantity: Quantity::One, signing_pubkey,
131+
},
132+
metadata_strategy: core::marker::PhantomData,
133+
secp_ctx: None,
134+
}
135+
}
136+
137+
/// Sets the [`Offer::metadata`] to the given bytes.
138+
///
139+
/// Successive calls to this method will override the previous setting.
140+
pub fn metadata(mut self, metadata: Vec<u8>) -> Result<Self, SemanticError> {
141+
self.offer.metadata = Some(Metadata::Bytes(metadata));
142+
Ok(self)
112143
}
144+
}
145+
146+
impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
147+
/// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing
148+
/// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides
149+
/// recipient privacy by using a different signing pubkey for each offer. Otherwise, the
150+
/// provided `node_id` is used for the signing pubkey.
151+
///
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`].
154+
///
155+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
156+
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
157+
pub fn deriving_signing_pubkey<ES: Deref>(
158+
description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
159+
secp_ctx: &'a Secp256k1<T>
160+
) -> Self where ES::Target: EntropySource {
161+
let nonce = Nonce::from_entropy_source(entropy_source);
162+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
163+
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
164+
OfferBuilder {
165+
offer: OfferContents {
166+
chains: None, metadata: Some(metadata), amount: None, description,
167+
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
168+
supported_quantity: Quantity::One, signing_pubkey: node_id,
169+
},
170+
metadata_strategy: core::marker::PhantomData,
171+
secp_ctx: Some(secp_ctx),
172+
}
173+
}
174+
}
113175

176+
impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
114177
/// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called,
115178
/// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported.
116179
///
@@ -127,14 +190,6 @@ impl OfferBuilder {
127190
self
128191
}
129192

130-
/// Sets the [`Offer::metadata`].
131-
///
132-
/// Successive calls to this method will override the previous setting.
133-
pub fn metadata(mut self, metadata: Vec<u8>) -> Self {
134-
self.offer.metadata = Some(metadata);
135-
self
136-
}
137-
138193
/// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`].
139194
///
140195
/// Successive calls to this method will override the previous setting.
@@ -204,28 +259,48 @@ impl OfferBuilder {
204259
}
205260
}
206261

262+
Ok(self.build_without_checks())
263+
}
264+
265+
fn build_without_checks(mut self) -> Offer {
266+
// Create the metadata for stateless verification of an InvoiceRequest.
267+
if let Some(mut metadata) = self.offer.metadata.take() {
268+
if metadata.has_derivation_material() {
269+
if self.offer.paths.is_none() {
270+
metadata = metadata.without_keys();
271+
}
272+
273+
let mut tlv_stream = self.offer.as_tlv_stream();
274+
debug_assert_eq!(tlv_stream.metadata, None);
275+
tlv_stream.metadata = None;
276+
tlv_stream.node_id = None;
277+
278+
let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
279+
metadata = derived_metadata;
280+
if let Some(keys) = keys {
281+
self.offer.signing_pubkey = keys.public_key();
282+
}
283+
}
284+
285+
self.offer.metadata = Some(metadata);
286+
}
287+
207288
let mut bytes = Vec::new();
208289
self.offer.write(&mut bytes).unwrap();
209290

210-
Ok(Offer {
211-
bytes,
212-
contents: self.offer,
213-
})
291+
Offer { bytes, contents: self.offer }
214292
}
215293
}
216294

217295
#[cfg(test)]
218-
impl OfferBuilder {
296+
impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
219297
fn features_unchecked(mut self, features: OfferFeatures) -> Self {
220298
self.offer.features = features;
221299
self
222300
}
223301

224302
pub(super) fn build_unchecked(self) -> Offer {
225-
let mut bytes = Vec::new();
226-
self.offer.write(&mut bytes).unwrap();
227-
228-
Offer { bytes, contents: self.offer }
303+
self.build_without_checks()
229304
}
230305
}
231306

@@ -242,7 +317,8 @@ impl OfferBuilder {
242317
///
243318
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
244319
/// [`Invoice`]: crate::offers::invoice::Invoice
245-
#[derive(Clone, Debug, PartialEq)]
320+
#[derive(Clone, Debug)]
321+
#[cfg_attr(test, derive(PartialEq))]
246322
pub struct Offer {
247323
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
248324
// fields.
@@ -254,10 +330,11 @@ pub struct Offer {
254330
///
255331
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
256332
/// [`Invoice`]: crate::offers::invoice::Invoice
257-
#[derive(Clone, Debug, PartialEq)]
333+
#[derive(Clone, Debug)]
334+
#[cfg_attr(test, derive(PartialEq))]
258335
pub(super) struct OfferContents {
259336
chains: Option<Vec<ChainHash>>,
260-
metadata: Option<Vec<u8>>,
337+
metadata: Option<Metadata>,
261338
amount: Option<Amount>,
262339
description: String,
263340
features: OfferFeatures,
@@ -292,7 +369,7 @@ impl Offer {
292369
/// Opaque bytes set by the originator. Useful for authentication and validating fields since it
293370
/// is reflected in `invoice_request` messages along with all the other fields from the `offer`.
294371
pub fn metadata(&self) -> Option<&Vec<u8>> {
295-
self.contents.metadata.as_ref()
372+
self.contents.metadata()
296373
}
297374

298375
/// The minimum amount required for a successful payment of a single item.
@@ -406,6 +483,10 @@ impl OfferContents {
406483
self.chains().contains(&chain)
407484
}
408485

486+
pub fn metadata(&self) -> Option<&Vec<u8>> {
487+
self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
488+
}
489+
409490
#[cfg(feature = "std")]
410491
pub(super) fn is_expired(&self) -> bool {
411492
match self.absolute_expiry {
@@ -498,7 +579,7 @@ impl OfferContents {
498579

499580
OfferTlvStreamRef {
500581
chains: self.chains.as_ref(),
501-
metadata: self.metadata.as_ref(),
582+
metadata: self.metadata(),
502583
currency,
503584
amount,
504585
description: Some(&self.description),
@@ -616,6 +697,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
616697
issuer, quantity_max, node_id,
617698
} = tlv_stream;
618699

700+
let metadata = metadata.map(|metadata| Metadata::Bytes(metadata));
701+
619702
let amount = match (currency, amount) {
620703
(None, None) => None,
621704
(None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => {
@@ -765,15 +848,15 @@ mod tests {
765848
#[test]
766849
fn builds_offer_with_metadata() {
767850
let offer = OfferBuilder::new("foo".into(), pubkey(42))
768-
.metadata(vec![42; 32])
851+
.metadata(vec![42; 32]).unwrap()
769852
.build()
770853
.unwrap();
771854
assert_eq!(offer.metadata(), Some(&vec![42; 32]));
772855
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
773856

774857
let offer = OfferBuilder::new("foo".into(), pubkey(42))
775-
.metadata(vec![42; 32])
776-
.metadata(vec![43; 32])
858+
.metadata(vec![42; 32]).unwrap()
859+
.metadata(vec![43; 32]).unwrap()
777860
.build()
778861
.unwrap();
779862
assert_eq!(offer.metadata(), Some(&vec![43; 32]));

lightning/src/offers/payer.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use crate::prelude::*;
1717
/// [`InvoiceRequest::payer_id`].
1818
///
1919
/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
20-
#[derive(Clone, Debug, PartialEq)]
20+
#[derive(Clone, Debug)]
21+
#[cfg_attr(test, derive(PartialEq))]
2122
pub(super) struct PayerContents(pub Vec<u8>);
2223

2324
tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {

0 commit comments

Comments
 (0)