Skip to content

Commit 6f60d2e

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 6f60d2e

File tree

8 files changed

+332
-42
lines changed

8 files changed

+332
-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: 119 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+
}
113145

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+
}
175+
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,50 @@ 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+
if metadata.derives_keys() {
277+
tlv_stream.node_id = None;
278+
}
279+
280+
let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
281+
metadata = derived_metadata;
282+
if let Some(keys) = keys {
283+
self.offer.signing_pubkey = keys.public_key();
284+
}
285+
}
286+
287+
self.offer.metadata = Some(metadata);
288+
}
289+
207290
let mut bytes = Vec::new();
208291
self.offer.write(&mut bytes).unwrap();
209292

210-
Ok(Offer {
211-
bytes,
212-
contents: self.offer,
213-
})
293+
Offer { bytes, contents: self.offer }
214294
}
215295
}
216296

217297
#[cfg(test)]
218-
impl OfferBuilder {
298+
impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
219299
fn features_unchecked(mut self, features: OfferFeatures) -> Self {
220300
self.offer.features = features;
221301
self
222302
}
223303

224304
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 }
305+
self.build_without_checks()
229306
}
230307
}
231308

@@ -242,7 +319,8 @@ impl OfferBuilder {
242319
///
243320
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
244321
/// [`Invoice`]: crate::offers::invoice::Invoice
245-
#[derive(Clone, Debug, PartialEq)]
322+
#[derive(Clone, Debug)]
323+
#[cfg_attr(test, derive(PartialEq))]
246324
pub struct Offer {
247325
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
248326
// fields.
@@ -254,10 +332,11 @@ pub struct Offer {
254332
///
255333
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
256334
/// [`Invoice`]: crate::offers::invoice::Invoice
257-
#[derive(Clone, Debug, PartialEq)]
335+
#[derive(Clone, Debug)]
336+
#[cfg_attr(test, derive(PartialEq))]
258337
pub(super) struct OfferContents {
259338
chains: Option<Vec<ChainHash>>,
260-
metadata: Option<Vec<u8>>,
339+
metadata: Option<Metadata>,
261340
amount: Option<Amount>,
262341
description: String,
263342
features: OfferFeatures,
@@ -292,7 +371,7 @@ impl Offer {
292371
/// Opaque bytes set by the originator. Useful for authentication and validating fields since it
293372
/// is reflected in `invoice_request` messages along with all the other fields from the `offer`.
294373
pub fn metadata(&self) -> Option<&Vec<u8>> {
295-
self.contents.metadata.as_ref()
374+
self.contents.metadata()
296375
}
297376

298377
/// The minimum amount required for a successful payment of a single item.
@@ -406,6 +485,10 @@ impl OfferContents {
406485
self.chains().contains(&chain)
407486
}
408487

488+
pub fn metadata(&self) -> Option<&Vec<u8>> {
489+
self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
490+
}
491+
409492
#[cfg(feature = "std")]
410493
pub(super) fn is_expired(&self) -> bool {
411494
match self.absolute_expiry {
@@ -498,7 +581,7 @@ impl OfferContents {
498581

499582
OfferTlvStreamRef {
500583
chains: self.chains.as_ref(),
501-
metadata: self.metadata.as_ref(),
584+
metadata: self.metadata(),
502585
currency,
503586
amount,
504587
description: Some(&self.description),
@@ -616,6 +699,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
616699
issuer, quantity_max, node_id,
617700
} = tlv_stream;
618701

702+
let metadata = metadata.map(|metadata| Metadata::Bytes(metadata));
703+
619704
let amount = match (currency, amount) {
620705
(None, None) => None,
621706
(None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => {
@@ -765,15 +850,15 @@ mod tests {
765850
#[test]
766851
fn builds_offer_with_metadata() {
767852
let offer = OfferBuilder::new("foo".into(), pubkey(42))
768-
.metadata(vec![42; 32])
853+
.metadata(vec![42; 32]).unwrap()
769854
.build()
770855
.unwrap();
771856
assert_eq!(offer.metadata(), Some(&vec![42; 32]));
772857
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
773858

774859
let offer = OfferBuilder::new("foo".into(), pubkey(42))
775-
.metadata(vec![42; 32])
776-
.metadata(vec![43; 32])
860+
.metadata(vec![42; 32]).unwrap()
861+
.metadata(vec![43; 32]).unwrap()
777862
.build()
778863
.unwrap();
779864
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)