Skip to content

Commit 59f30c2

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 4ed63ed commit 59f30c2

File tree

4 files changed

+195
-9
lines changed

4 files changed

+195
-9
lines changed

lightning/src/ln/inbound_payment.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use alloc::string::ToString;
1313
use bitcoin::hashes::{Hash, HashEngine};
1414
use bitcoin::hashes::cmp::fixed_time_eq;
1515
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
16-
use bitcoin::hashes::sha256::Hash as Sha256;
16+
use bitcoin::hashes::sha256::{Hash as Sha256, self};
17+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
1718
use crate::chain::keysinterface::{KeyMaterial, EntropySource};
1819
use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
1920
use crate::ln::msgs;
@@ -66,6 +67,59 @@ impl ExpandedKey {
6667
offers_base_key,
6768
}
6869
}
70+
71+
/// Returns an [`HmacEngine`] used to construct [`Offer::metadata`].
72+
///
73+
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
74+
#[allow(unused)]
75+
pub(crate) fn hmac_for_offer(&self, nonce: Nonce) -> HmacEngine<Sha256> {
76+
let mut hmac = HmacEngine::<Sha256>::new(&self.offers_base_key);
77+
hmac.input(&nonce.0);
78+
hmac
79+
}
80+
81+
/// Derives a pubkey using the given nonce for use as [`Offer::signing_pubkey`].
82+
///
83+
/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
84+
#[allow(unused)]
85+
pub(crate) fn signing_pubkey_for_offer(&self, nonce: Nonce) -> PublicKey {
86+
let mut engine = sha256::Hash::engine();
87+
engine.input(&self.offers_base_key);
88+
engine.input(&nonce.0);
89+
90+
let hash = sha256::Hash::from_engine(engine);
91+
let secp_ctx = Secp256k1::new();
92+
SecretKey::from_slice(&hash).unwrap().public_key(&secp_ctx)
93+
}
94+
}
95+
96+
/// A 128-bit number used only once.
97+
///
98+
/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from
99+
/// [`ExpandedKey`].
100+
///
101+
/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata
102+
/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey
103+
#[allow(unused)]
104+
#[derive(Clone, Copy)]
105+
pub struct Nonce(pub(crate) [u8; Self::LENGTH]);
106+
107+
impl Nonce {
108+
/// Number of bytes in the nonce.
109+
pub const LENGTH: usize = 16;
110+
111+
/// Creates a `Nonce` from the given [`EntropySource`].
112+
pub fn from_entropy_source<ES: EntropySource>(entropy_source: &ES) -> Self {
113+
let mut bytes = [0u8; Self::LENGTH];
114+
let rand_bytes = entropy_source.get_secure_random_bytes();
115+
bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]);
116+
Nonce(bytes)
117+
}
118+
119+
/// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`].
120+
pub fn as_slice(&self) -> &[u8] {
121+
&self.0
122+
}
69123
}
70124

71125
enum Method {

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: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ use core::str::FromStr;
7575
use core::time::Duration;
7676
use crate::io;
7777
use crate::ln::features::OfferFeatures;
78+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
7879
use crate::ln::msgs::MAX_VALUE_MSAT;
7980
use crate::offers::invoice_request::InvoiceRequestBuilder;
8081
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
82+
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
8183
use crate::onion_message::BlindedPath;
8284
use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
8385
use crate::util::string::PrintableString;
@@ -94,6 +96,7 @@ use std::time::SystemTime;
9496
/// [module-level documentation]: self
9597
pub struct OfferBuilder {
9698
offer: OfferContents,
99+
metadata_material: Option<MetadataMaterial>,
97100
}
98101

99102
impl OfferBuilder {
@@ -108,7 +111,28 @@ impl OfferBuilder {
108111
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
109112
supported_quantity: Quantity::One, signing_pubkey,
110113
};
111-
OfferBuilder { offer }
114+
OfferBuilder { offer, metadata_material: None }
115+
}
116+
117+
/// Similar to [`OfferBuilder::new`] except it:
118+
/// - derives the signing pubkey such that a different key can be used for each offer, and
119+
/// - sets the metadata when [`OfferBuilder::build`] is called such that it can be used by
120+
/// [`InvoiceRequest::verify`] to determine if the request was produced using a base
121+
/// [`ExpandedKey`] from which the signing pubkey was derived.
122+
///
123+
/// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify
124+
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
125+
#[allow(unused)]
126+
pub(crate) fn deriving_signing_pubkey(
127+
description: String, signing_pubkey: DerivedPubkey
128+
) -> Self {
129+
let (signing_pubkey, metadata_material) = signing_pubkey.into_parts();
130+
let offer = OfferContents {
131+
chains: None, metadata: None, amount: None, description,
132+
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
133+
supported_quantity: Quantity::One, signing_pubkey,
134+
};
135+
OfferBuilder { offer, metadata_material: Some(metadata_material) }
112136
}
113137

114138
/// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called,
@@ -127,12 +151,38 @@ impl OfferBuilder {
127151
self
128152
}
129153

130-
/// Sets the [`Offer::metadata`].
154+
/// Sets the [`Offer::metadata`] to the given bytes.
131155
///
132-
/// Successive calls to this method will override the previous setting.
133-
pub fn metadata(mut self, metadata: Vec<u8>) -> Self {
156+
/// Successive calls to this method will override the previous setting. Errors if the builder
157+
/// was constructed using a derived pubkey.
158+
pub fn metadata(mut self, metadata: Vec<u8>) -> Result<Self, SemanticError> {
159+
if self.metadata_material.is_some() {
160+
return Err(SemanticError::UnexpectedMetadata);
161+
}
162+
134163
self.offer.metadata = Some(metadata);
135-
self
164+
Ok(self)
165+
}
166+
167+
/// Sets the [`Offer::metadata`] derived from the given `key` and any fields set prior to
168+
/// calling [`OfferBuilder::build`]. Allows for stateless verification of an [`InvoiceRequest`]
169+
/// when using a public node id as the [`Offer::signing_pubkey`] instead of a derived one.
170+
///
171+
/// Errors if already called or if the builder was constructed with
172+
/// [`Self::deriving_signing_pubkey`].
173+
///
174+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
175+
#[allow(unused)]
176+
pub(crate) fn metadata_derived(
177+
mut self, key: &ExpandedKey, nonce: Nonce
178+
) -> Result<Self, SemanticError> {
179+
if self.metadata_material.is_some() {
180+
return Err(SemanticError::UnexpectedMetadata);
181+
}
182+
183+
self.offer.metadata = None;
184+
self.metadata_material = Some(MetadataMaterial::new(nonce, key));
185+
Ok(self)
136186
}
137187

138188
/// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`].
@@ -204,6 +254,16 @@ impl OfferBuilder {
204254
}
205255
}
206256

257+
// Create the metadata for stateless verification of an InvoiceRequest.
258+
if let Some(mut metadata_material) = self.metadata_material {
259+
debug_assert!(self.offer.metadata.is_none());
260+
let mut tlv_stream = self.offer.as_tlv_stream();
261+
tlv_stream.node_id = None;
262+
tlv_stream.write(&mut metadata_material).unwrap();
263+
264+
self.offer.metadata = Some(metadata_material.into_metadata());
265+
}
266+
207267
let mut bytes = Vec::new();
208268
self.offer.write(&mut bytes).unwrap();
209269

@@ -764,15 +824,15 @@ mod tests {
764824
#[test]
765825
fn builds_offer_with_metadata() {
766826
let offer = OfferBuilder::new("foo".into(), pubkey(42))
767-
.metadata(vec![42; 32])
827+
.metadata(vec![42; 32]).unwrap()
768828
.build()
769829
.unwrap();
770830
assert_eq!(offer.metadata(), Some(&vec![42; 32]));
771831
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
772832

773833
let offer = OfferBuilder::new("foo".into(), pubkey(42))
774-
.metadata(vec![42; 32])
775-
.metadata(vec![43; 32])
834+
.metadata(vec![42; 32]).unwrap()
835+
.metadata(vec![43; 32]).unwrap()
776836
.build()
777837
.unwrap();
778838
assert_eq!(offer.metadata(), Some(&vec![43; 32]));

lightning/src/offers/signer.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Utilities for signing offer messages and verifying metadata.
11+
12+
use bitcoin::hashes::{Hash, HashEngine};
13+
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
14+
use bitcoin::hashes::sha256::Hash as Sha256;
15+
use core::convert::TryInto;
16+
use bitcoin::secp256k1::PublicKey;
17+
use crate::io;
18+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
19+
20+
/// A pubkey derived from a base key and nonce. Used to crate metadata for a message such that it
21+
/// can be verified using [`verify_metadata`].
22+
pub(crate) struct DerivedPubkey {
23+
public_key: PublicKey,
24+
metadata_material: MetadataMaterial,
25+
}
26+
27+
impl DerivedPubkey {
28+
pub(crate) fn new(expanded_key: &ExpandedKey, nonce: Nonce) -> Self {
29+
Self {
30+
public_key: expanded_key.signing_pubkey_for_offer(nonce),
31+
metadata_material: MetadataMaterial::new(nonce, expanded_key),
32+
}
33+
}
34+
35+
pub(super) fn into_parts(self) -> (PublicKey, MetadataMaterial) {
36+
(self.public_key, self.metadata_material)
37+
}
38+
}
39+
40+
/// Material used to create metadata for a message. Once initialized, write the applicable data from
41+
/// the message into it and call [`MetadataMaterial::into_metadata`].
42+
pub(super) struct MetadataMaterial {
43+
nonce: Nonce,
44+
hmac: HmacEngine<Sha256>,
45+
}
46+
47+
impl MetadataMaterial {
48+
pub fn new(nonce: Nonce, expanded_key: &ExpandedKey) -> Self {
49+
Self {
50+
nonce,
51+
hmac: expanded_key.hmac_for_offer(nonce),
52+
}
53+
}
54+
55+
pub fn into_metadata(self) -> Vec<u8> {
56+
let mut bytes = self.nonce.as_slice().to_vec();
57+
bytes.extend_from_slice(&Hmac::from_engine(self.hmac).into_inner());
58+
bytes
59+
}
60+
}
61+
62+
impl io::Write for MetadataMaterial {
63+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
64+
self.hmac.write(buf)
65+
}
66+
67+
fn flush(&mut self) -> io::Result<()> {
68+
self.hmac.flush()
69+
}
70+
}

0 commit comments

Comments
 (0)