From ce7a02d1e002e5047aa1bf56a0176d85d8a5bde3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 2 Feb 2023 17:13:09 -0600 Subject: [PATCH 01/16] Common offers test_utils module Move utility functions used across all offers modules into a common module. Avoids duplicating larger utilities such as payment_path across more than one module. --- lightning/src/offers/invoice.rs | 103 ++-------------------- lightning/src/offers/invoice_request.rs | 30 +------ lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 11 +-- lightning/src/offers/refund.rs | 17 +--- lightning/src/offers/test_utils.rs | 110 ++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 151 deletions(-) create mode 100644 lightning/src/offers/test_utils.rs diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 48b8cec3536..2c530760ce3 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -777,68 +777,27 @@ impl TryFrom for InvoiceContents { #[cfg(test)] mod tests { - use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; + use super::{DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, self}; - use bitcoin::secp256k1::schnorr::Signature; + use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self}; use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; - use core::convert::{Infallible, TryFrom}; + use core::convert::TryFrom; use core::time::Duration; - use crate::ln::PaymentHash; use crate::ln::msgs::DecodeError; - use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; + use crate::ln::features::Bolt12InvoiceFeatures; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; - use crate::onion_message::{BlindedHop, BlindedPath}; + use crate::offers::test_utils::*; use crate::util::ser::{BigSize, Iterable, Writeable}; - fn payer_keys() -> KeyPair { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) - } - - fn payer_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn payer_pubkey() -> PublicKey { - payer_keys().public_key() - } - - fn recipient_keys() -> KeyPair { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) - } - - fn recipient_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn recipient_pubkey() -> PublicKey { - recipient_keys().public_key() - } - - fn pubkey(byte: u8) -> PublicKey { - let secp_ctx = Secp256k1::new(); - PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) - } - - fn privkey(byte: u8) -> SecretKey { - SecretKey::from_slice(&[byte; 32]).unwrap() - } - trait ToBytes { fn to_bytes(&self) -> Vec; } @@ -855,58 +814,6 @@ mod tests { } } - fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { - let paths = vec![ - BlindedPath { - introduction_node_id: pubkey(40), - blinding_point: pubkey(41), - blinded_hops: vec![ - BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, - BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, - ], - }, - BlindedPath { - introduction_node_id: pubkey(40), - blinding_point: pubkey(41), - blinded_hops: vec![ - BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, - BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, - ], - }, - ]; - - let payinfo = vec![ - BlindedPayInfo { - fee_base_msat: 1, - fee_proportional_millionths: 1_000, - cltv_expiry_delta: 42, - htlc_minimum_msat: 100, - htlc_maximum_msat: 1_000_000_000_000, - features: BlindedHopFeatures::empty(), - }, - BlindedPayInfo { - fee_base_msat: 1, - fee_proportional_millionths: 1_000, - cltv_expiry_delta: 42, - htlc_minimum_msat: 100, - htlc_maximum_msat: 1_000_000_000_000, - features: BlindedHopFeatures::empty(), - }, - ]; - - paths.into_iter().zip(payinfo.into_iter()).collect() - } - - fn payment_hash() -> PaymentHash { - PaymentHash([42; 32]) - } - - fn now() -> Duration { - std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") - } - #[test] fn builds_invoice_for_offer_with_defaults() { let payment_paths = payment_paths(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index a1a0520c622..8364814ede3 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -532,8 +532,7 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, self}; - use bitcoin::secp256k1::schnorr::Signature; + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey, self}; use core::convert::{Infallible, TryFrom}; use core::num::NonZeroU64; #[cfg(feature = "std")] @@ -544,35 +543,10 @@ mod tests { use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; + use crate::offers::test_utils::*; use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; - fn payer_keys() -> KeyPair { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) - } - - fn payer_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn payer_pubkey() -> PublicKey { - payer_keys().public_key() - } - - fn recipient_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn recipient_pubkey() -> PublicKey { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()).public_key() - } - #[test] fn builds_invoice_request_with_defaults() { let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 2da6fac08ff..c2b0d6aea18 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -19,3 +19,5 @@ pub mod offer; pub mod parse; mod payer; pub mod refund; +#[cfg(test)] +mod test_utils; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 405e2e278d8..a1445c6f792 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -666,26 +666,17 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use core::convert::TryFrom; use core::num::NonZeroU64; use core::time::Duration; use crate::ln::features::OfferFeatures; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::parse::{ParseError, SemanticError}; + use crate::offers::test_utils::*; use crate::onion_message::{BlindedHop, BlindedPath}; use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; - fn pubkey(byte: u8) -> PublicKey { - let secp_ctx = Secp256k1::new(); - PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) - } - - fn privkey(byte: u8) -> SecretKey { - SecretKey::from_slice(&[byte; 32]).unwrap() - } - #[test] fn builds_offer_with_defaults() { let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index cc0388c0241..51cfebed4a8 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -575,7 +575,7 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; use core::convert::TryFrom; use core::time::Duration; use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; @@ -584,24 +584,11 @@ mod tests { use crate::offers::offer::OfferTlvStreamRef; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; + use crate::offers::test_utils::*; use crate::onion_message::{BlindedHop, BlindedPath}; use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; - fn payer_pubkey() -> PublicKey { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()).public_key() - } - - fn pubkey(byte: u8) -> PublicKey { - let secp_ctx = Secp256k1::new(); - PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) - } - - fn privkey(byte: u8) -> SecretKey { - SecretKey::from_slice(&[byte; 32]).unwrap() - } - trait ToBytes { fn to_bytes(&self) -> Vec; } diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs new file mode 100644 index 00000000000..7447b86fb84 --- /dev/null +++ b/lightning/src/offers/test_utils.rs @@ -0,0 +1,110 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Utilities for testing BOLT 12 Offers interfaces + +use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::schnorr::Signature; +use core::convert::Infallible; +use core::time::Duration; +use crate::ln::PaymentHash; +use crate::ln::features::BlindedHopFeatures; +use crate::offers::invoice::BlindedPayInfo; +use crate::onion_message::{BlindedHop, BlindedPath}; + +pub(super) fn payer_keys() -> KeyPair { + let secp_ctx = Secp256k1::new(); + KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) +} + +pub(super) fn payer_sign(digest: &Message) -> Result { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) +} + +pub(super) fn payer_pubkey() -> PublicKey { + payer_keys().public_key() +} + +pub(super) fn recipient_keys() -> KeyPair { + let secp_ctx = Secp256k1::new(); + KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) +} + +pub(super) fn recipient_sign(digest: &Message) -> Result { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) +} + +pub(super) fn recipient_pubkey() -> PublicKey { + recipient_keys().public_key() +} + +pub(super) fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +pub(super) fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + +pub(super) fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { + let paths = vec![ + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }, + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }, + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + paths.into_iter().zip(payinfo.into_iter()).collect() +} + +pub(super) fn payment_hash() -> PaymentHash { + PaymentHash([42; 32]) +} + +pub(super) fn now() -> Duration { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") +} From fd426a0018bf017d379eba26d922f6774e9d10e6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 6 Feb 2023 12:55:54 -0600 Subject: [PATCH 02/16] Add missing UnknownRequiredFeatures tests --- lightning/src/offers/invoice_request.rs | 16 ++++++++++++++++ lightning/src/offers/refund.rs | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 8364814ede3..db540ce62ff 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -982,6 +982,22 @@ mod tests { } } + #[test] + fn fails_responding_with_unknown_required_features() { + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![42; 32], payer_pubkey()).unwrap() + .features_unchecked(InvoiceRequestFeatures::unknown()) + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures), + } + } + #[test] fn parses_invoice_request_with_metadata() { let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 51cfebed4a8..9eda6ecd50c 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -809,6 +809,18 @@ mod tests { assert_eq!(tlv_stream.payer_note, Some(&String::from("baz"))); } + #[test] + fn fails_responding_with_unknown_required_features() { + match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .features_unchecked(InvoiceRequestFeatures::unknown()) + .build().unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures), + } + } + #[test] fn parses_refund_with_metadata() { let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() From 336fc023edf3a6cf11cf630898346a7de03bd0f6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 7 Feb 2023 15:25:36 -0600 Subject: [PATCH 03/16] Add another ExpandedKey derivation for Offers To support transient signing pubkeys and payer ids for Offers, add another key derivation to ExpandedKey. Also useful for constructing metadata for stateless message authentication. --- lightning/src/ln/inbound_payment.rs | 9 ++++++--- lightning/src/util/crypto.rs | 13 +++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 0c6d6f2b804..058339cbc1d 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -19,7 +19,7 @@ use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::msgs; use crate::ln::msgs::MAX_VALUE_MSAT; use crate::util::chacha20::ChaCha20; -use crate::util::crypto::hkdf_extract_expand_thrice; +use crate::util::crypto::hkdf_extract_expand_4x; use crate::util::errors::APIError; use crate::util::logger::Logger; @@ -48,6 +48,8 @@ pub struct ExpandedKey { /// The key used to authenticate a user-provided payment hash and metadata as previously /// registered with LDK. user_pmt_hash_key: [u8; 32], + /// The base key used to derive signing keys and authenticate messages for BOLT 12 Offers. + offers_base_key: [u8; 32], } impl ExpandedKey { @@ -55,12 +57,13 @@ impl ExpandedKey { /// /// It is recommended to cache this value and not regenerate it for each new inbound payment. pub fn new(key_material: &KeyMaterial) -> ExpandedKey { - let (metadata_key, ldk_pmt_hash_key, user_pmt_hash_key) = - hkdf_extract_expand_thrice(b"LDK Inbound Payment Key Expansion", &key_material.0); + let (metadata_key, ldk_pmt_hash_key, user_pmt_hash_key, offers_base_key) = + hkdf_extract_expand_4x(b"LDK Inbound Payment Key Expansion", &key_material.0); Self { metadata_key, ldk_pmt_hash_key, user_pmt_hash_key, + offers_base_key, } } } diff --git a/lightning/src/util/crypto.rs b/lightning/src/util/crypto.rs index 2f2d33b29f7..39dfd39b785 100644 --- a/lightning/src/util/crypto.rs +++ b/lightning/src/util/crypto.rs @@ -20,13 +20,18 @@ macro_rules! hkdf_extract_expand { let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm); (k1, k2) }}; - ($salt: expr, $ikm: expr, 3) => {{ + ($salt: expr, $ikm: expr, 4) => {{ let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm); let mut hmac = HmacEngine::::new(&prk[..]); hmac.input(&k2); hmac.input(&[3; 1]); - (k1, k2, Hmac::from_engine(hmac).into_inner()) + let k3 = Hmac::from_engine(hmac).into_inner(); + + let mut hmac = HmacEngine::::new(&prk[..]); + hmac.input(&k3); + hmac.input(&[4; 1]); + (k1, k2, k3, Hmac::from_engine(hmac).into_inner()) }} } @@ -34,8 +39,8 @@ pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32] hkdf_extract_expand!(salt, ikm, 2) } -pub fn hkdf_extract_expand_thrice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32], [u8; 32]) { - hkdf_extract_expand!(salt, ikm, 3) +pub fn hkdf_extract_expand_4x(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32]) { + hkdf_extract_expand!(salt, ikm, 4) } #[inline] From 1cad430e14108710c826adebbfab2a5ea64a6a5a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 7 Feb 2023 19:13:08 -0600 Subject: [PATCH 04/16] 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. --- lightning/src/ln/inbound_payment.rs | 48 +++++++- lightning/src/offers/invoice.rs | 6 +- lightning/src/offers/invoice_request.rs | 6 +- lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 153 ++++++++++++++++++------ lightning/src/offers/payer.rs | 3 +- lightning/src/offers/refund.rs | 6 +- lightning/src/offers/signer.rs | 150 +++++++++++++++++++++++ 8 files changed, 332 insertions(+), 42 deletions(-) create mode 100644 lightning/src/offers/signer.rs diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 058339cbc1d..e6668a33cee 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -26,7 +26,7 @@ use crate::util::logger::Logger; use core::convert::TryInto; use core::ops::Deref; -const IV_LEN: usize = 16; +pub(crate) const IV_LEN: usize = 16; const METADATA_LEN: usize = 16; const METADATA_KEY_LEN: usize = 32; const AMT_MSAT_LEN: usize = 8; @@ -66,6 +66,52 @@ impl ExpandedKey { offers_base_key, } } + + /// Returns an [`HmacEngine`] used to construct [`Offer::metadata`]. + /// + /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata + #[allow(unused)] + pub(crate) fn hmac_for_offer( + &self, nonce: Nonce, iv_bytes: &[u8; IV_LEN] + ) -> HmacEngine { + let mut hmac = HmacEngine::::new(&self.offers_base_key); + hmac.input(iv_bytes); + hmac.input(&nonce.0); + hmac + } +} + +/// A 128-bit number used only once. +/// +/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from +/// [`ExpandedKey`]. Must not be reused for any other derivation without first hashing. +/// +/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata +/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey +#[allow(unused)] +#[derive(Clone, Copy)] +pub(crate) struct Nonce([u8; Self::LENGTH]); + +impl Nonce { + /// Number of bytes in the nonce. + pub const LENGTH: usize = 16; + + /// Creates a `Nonce` from the given [`EntropySource`]. + pub fn from_entropy_source(entropy_source: ES) -> Self + where + ES::Target: EntropySource, + { + let mut bytes = [0u8; Self::LENGTH]; + let rand_bytes = entropy_source.get_secure_random_bytes(); + bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]); + + Nonce(bytes) + } + + /// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`]. + pub fn as_slice(&self) -> &[u8] { + &self.0 + } } enum Method { diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 2c530760ce3..b0783a306e5 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -313,7 +313,8 @@ impl<'a> UnsignedInvoice<'a> { /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Invoice { bytes: Vec, contents: InvoiceContents, @@ -324,7 +325,8 @@ pub struct Invoice { /// /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] enum InvoiceContents { /// Contents for an [`Invoice`] corresponding to an [`Offer`]. /// diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index db540ce62ff..8bb5737c3f7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -250,7 +250,8 @@ impl<'a> UnsignedInvoiceRequest<'a> { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct InvoiceRequest { pub(super) bytes: Vec, pub(super) contents: InvoiceRequestContents, @@ -260,7 +261,8 @@ pub struct InvoiceRequest { /// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub(super) struct InvoiceRequestContents { payer: PayerContents, pub(super) offer: OfferContents, diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index c2b0d6aea18..0fb20f42d79 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -19,5 +19,7 @@ pub mod offer; pub mod parse; mod payer; pub mod refund; +#[allow(unused)] +pub(crate) mod signer; #[cfg(test)] mod test_utils; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a1445c6f792..a5935c87b8a 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -68,16 +68,20 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{PublicKey, Secp256k1, self}; use core::convert::TryFrom; use core::num::NonZeroU64; +use core::ops::Deref; use core::str::FromStr; use core::time::Duration; +use crate::chain::keysinterface::EntropySource; use crate::io; use crate::ln::features::OfferFeatures; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::MAX_VALUE_MSAT; use crate::offers::invoice_request::InvoiceRequestBuilder; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -87,30 +91,89 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; + /// Builds an [`Offer`] for the "offer to be paid" flow. /// /// See [module-level documentation] for usage. /// /// [module-level documentation]: self -pub struct OfferBuilder { +pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> { offer: OfferContents, + metadata_strategy: core::marker::PhantomData, + secp_ctx: Option<&'a Secp256k1>, } -impl OfferBuilder { +/// Indicates how [`Offer::metadata`] may be set. +pub trait MetadataStrategy {} + +/// [`Offer::metadata`] may be explicitly set or left empty. +pub struct ExplicitMetadata {} + +/// [`Offer::metadata`] will be derived. +pub struct DerivedMetadata {} + +impl MetadataStrategy for ExplicitMetadata {} +impl MetadataStrategy for DerivedMetadata {} + +impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> { /// Creates a new builder for an offer setting the [`Offer::description`] and using the /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered /// while the offer is valid. /// /// Use a different pubkey per offer to avoid correlating offers. pub fn new(description: String, signing_pubkey: PublicKey) -> Self { - let offer = OfferContents { - chains: None, metadata: None, amount: None, description, - features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, - supported_quantity: Quantity::One, signing_pubkey, - }; - OfferBuilder { offer } + OfferBuilder { + offer: OfferContents { + chains: None, metadata: None, amount: None, description, + features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, + supported_quantity: Quantity::One, signing_pubkey, + }, + metadata_strategy: core::marker::PhantomData, + secp_ctx: None, + } + } + + /// Sets the [`Offer::metadata`] to the given bytes. + /// + /// Successive calls to this method will override the previous setting. + pub fn metadata(mut self, metadata: Vec) -> Result { + self.offer.metadata = Some(Metadata::Bytes(metadata)); + Ok(self) } +} +impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { + /// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing + /// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides + /// recipient privacy by using a different signing pubkey for each offer. Otherwise, the + /// provided `node_id` is used for the signing pubkey. + /// + /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used to + /// verify that an [`InvoiceRequest`] was produced for the offer given an [`ExpandedKey`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey + pub fn deriving_signing_pubkey( + description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + secp_ctx: &'a Secp256k1 + ) -> Self where ES::Target: EntropySource { + let nonce = Nonce::from_entropy_source(entropy_source); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let metadata = Metadata::DerivedSigningPubkey(derivation_material); + OfferBuilder { + offer: OfferContents { + chains: None, metadata: Some(metadata), amount: None, description, + features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, + supported_quantity: Quantity::One, signing_pubkey: node_id, + }, + metadata_strategy: core::marker::PhantomData, + secp_ctx: Some(secp_ctx), + } + } +} + +impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> { /// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called, /// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported. /// @@ -127,14 +190,6 @@ impl OfferBuilder { self } - /// Sets the [`Offer::metadata`]. - /// - /// Successive calls to this method will override the previous setting. - pub fn metadata(mut self, metadata: Vec) -> Self { - self.offer.metadata = Some(metadata); - self - } - /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`]. /// /// Successive calls to this method will override the previous setting. @@ -204,28 +259,50 @@ impl OfferBuilder { } } + Ok(self.build_without_checks()) + } + + fn build_without_checks(mut self) -> Offer { + // Create the metadata for stateless verification of an InvoiceRequest. + if let Some(mut metadata) = self.offer.metadata.take() { + if metadata.has_derivation_material() { + if self.offer.paths.is_none() { + metadata = metadata.without_keys(); + } + + let mut tlv_stream = self.offer.as_tlv_stream(); + debug_assert_eq!(tlv_stream.metadata, None); + tlv_stream.metadata = None; + if metadata.derives_keys() { + tlv_stream.node_id = None; + } + + let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx); + metadata = derived_metadata; + if let Some(keys) = keys { + self.offer.signing_pubkey = keys.public_key(); + } + } + + self.offer.metadata = Some(metadata); + } + let mut bytes = Vec::new(); self.offer.write(&mut bytes).unwrap(); - Ok(Offer { - bytes, - contents: self.offer, - }) + Offer { bytes, contents: self.offer } } } #[cfg(test)] -impl OfferBuilder { +impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> { fn features_unchecked(mut self, features: OfferFeatures) -> Self { self.offer.features = features; self } pub(super) fn build_unchecked(self) -> Offer { - let mut bytes = Vec::new(); - self.offer.write(&mut bytes).unwrap(); - - Offer { bytes, contents: self.offer } + self.build_without_checks() } } @@ -242,7 +319,8 @@ impl OfferBuilder { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. @@ -254,10 +332,11 @@ pub struct Offer { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub(super) struct OfferContents { chains: Option>, - metadata: Option>, + metadata: Option, amount: Option, description: String, features: OfferFeatures, @@ -292,7 +371,7 @@ impl Offer { /// Opaque bytes set by the originator. Useful for authentication and validating fields since it /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. pub fn metadata(&self) -> Option<&Vec> { - self.contents.metadata.as_ref() + self.contents.metadata() } /// The minimum amount required for a successful payment of a single item. @@ -406,6 +485,10 @@ impl OfferContents { self.chains().contains(&chain) } + pub fn metadata(&self) -> Option<&Vec> { + self.metadata.as_ref().and_then(|metadata| metadata.as_bytes()) + } + #[cfg(feature = "std")] pub(super) fn is_expired(&self) -> bool { match self.absolute_expiry { @@ -498,7 +581,7 @@ impl OfferContents { OfferTlvStreamRef { chains: self.chains.as_ref(), - metadata: self.metadata.as_ref(), + metadata: self.metadata(), currency, amount, description: Some(&self.description), @@ -616,6 +699,8 @@ impl TryFrom for OfferContents { issuer, quantity_max, node_id, } = tlv_stream; + let metadata = metadata.map(|metadata| Metadata::Bytes(metadata)); + let amount = match (currency, amount) { (None, None) => None, (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => { @@ -765,15 +850,15 @@ mod tests { #[test] fn builds_offer_with_metadata() { let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .metadata(vec![42; 32]) + .metadata(vec![42; 32]).unwrap() .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![42; 32])); assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32])); let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .metadata(vec![42; 32]) - .metadata(vec![43; 32]) + .metadata(vec![42; 32]).unwrap() + .metadata(vec![43; 32]).unwrap() .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![43; 32])); diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 7e1da769eda..12b471c6ce4 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -17,7 +17,8 @@ use crate::prelude::*; /// [`InvoiceRequest::payer_id`]. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub(super) struct PayerContents(pub Vec); tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 9eda6ecd50c..6d44eb3da6d 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -216,7 +216,8 @@ impl RefundBuilder { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Refund { pub(super) bytes: Vec, pub(super) contents: RefundContents, @@ -225,7 +226,8 @@ pub struct Refund { /// The contents of a [`Refund`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub(super) struct RefundContents { payer: PayerContents, // offer fields diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs new file mode 100644 index 00000000000..e1a1a4dfd6c --- /dev/null +++ b/lightning/src/offers/signer.rs @@ -0,0 +1,150 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Utilities for signing offer messages and verifying metadata. + +use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey, self}; +use core::convert::TryInto; +use core::fmt; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::util::ser::Writeable; + +use crate::prelude::*; + +const DERIVED_METADATA_HMAC_INPUT: &[u8; 16] = &[1; 16]; +const DERIVED_METADATA_AND_KEYS_HMAC_INPUT: &[u8; 16] = &[2; 16]; + +/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be +/// verified. +#[derive(Clone)] +pub(super) enum Metadata { + /// Metadata as parsed, supplied by the user, or derived from the message contents. + Bytes(Vec), + + /// Metadata to be derived from message contents and given material. + Derived(MetadataMaterial), + + /// Metadata and signing pubkey to be derived from message contents and given material. + DerivedSigningPubkey(MetadataMaterial), +} + +impl Metadata { + pub fn as_bytes(&self) -> Option<&Vec> { + match self { + Metadata::Bytes(bytes) => Some(bytes), + Metadata::Derived(_) => None, + Metadata::DerivedSigningPubkey(_) => None, + } + } + + pub fn has_derivation_material(&self) -> bool { + match self { + Metadata::Bytes(_) => false, + Metadata::Derived(_) => true, + Metadata::DerivedSigningPubkey(_) => true, + } + } + + pub fn derives_keys(&self) -> bool { + match self { + Metadata::Bytes(_) => false, + Metadata::Derived(_) => false, + Metadata::DerivedSigningPubkey(_) => true, + } + } + + pub fn without_keys(self) -> Self { + match self { + Metadata::Bytes(_) => self, + Metadata::Derived(_) => self, + Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material), + } + } + + pub fn derive_from( + self, tlv_stream: W, secp_ctx: Option<&Secp256k1> + ) -> (Self, Option) { + match self { + Metadata::Bytes(_) => (self, None), + Metadata::Derived(mut metadata_material) => { + tlv_stream.write(&mut metadata_material.hmac).unwrap(); + (Metadata::Bytes(metadata_material.derive_metadata()), None) + }, + Metadata::DerivedSigningPubkey(mut metadata_material) => { + tlv_stream.write(&mut metadata_material.hmac).unwrap(); + let secp_ctx = secp_ctx.unwrap(); + let (metadata, keys) = metadata_material.derive_metadata_and_keys(secp_ctx); + (Metadata::Bytes(metadata), Some(keys)) + }, + } + } +} + +impl fmt::Debug for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Metadata::Bytes(bytes) => bytes.fmt(f), + Metadata::Derived(_) => f.write_str("Derived"), + Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"), + } + } +} + +#[cfg(test)] +impl PartialEq for Metadata { + fn eq(&self, other: &Self) -> bool { + match self { + Metadata::Bytes(bytes) => if let Metadata::Bytes(other_bytes) = other { + bytes == other_bytes + } else { + false + }, + Metadata::Derived(_) => false, + Metadata::DerivedSigningPubkey(_) => false, + } + } +} + +/// Material used to create metadata for a message. +#[derive(Clone)] +pub(super) struct MetadataMaterial { + nonce: Nonce, + hmac: HmacEngine, +} + +impl MetadataMaterial { + pub fn new(nonce: Nonce, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN]) -> Self { + Self { + nonce, + hmac: expanded_key.hmac_for_offer(nonce, iv_bytes), + } + } + + fn derive_metadata(mut self) -> Vec { + self.hmac.input(DERIVED_METADATA_HMAC_INPUT); + + let mut bytes = self.nonce.as_slice().to_vec(); + bytes.extend_from_slice(&Hmac::from_engine(self.hmac).into_inner()); + bytes + } + + fn derive_metadata_and_keys( + mut self, secp_ctx: &Secp256k1 + ) -> (Vec, KeyPair) { + self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); + + let hmac = Hmac::from_engine(self.hmac); + let privkey = SecretKey::from_slice(hmac.as_inner()).unwrap(); + let keys = KeyPair::from_secret_key(secp_ctx, &privkey); + (self.nonce.as_slice().to_vec(), keys) + } +} From b2e87ff25b2aef7860ac3c280adf06f642773600 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 25 Jan 2023 11:34:43 -0600 Subject: [PATCH 05/16] TlvStream range iterator Add an iterator that yields TlvRecords over a range of a TlvStream. Useful for verifying that, e.g., an InvoiceRequest was sent in response to an Offer constructed by the intended recipient. --- lightning/src/offers/merkle.rs | 53 ++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 94a1eac0ca4..f6827467420 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -143,28 +143,37 @@ fn tagged_branch_hash_from_engine( /// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a /// well-formed TLV stream. -struct TlvStream<'a> { +pub(super) struct TlvStream<'a> { data: io::Cursor<&'a [u8]>, } impl<'a> TlvStream<'a> { - fn new(data: &'a [u8]) -> Self { + pub fn new(data: &'a [u8]) -> Self { Self { data: io::Cursor::new(data), } } + pub fn range(self, types: T) -> impl core::iter::Iterator> + where + T: core::ops::RangeBounds + Clone, + { + let take_range = types.clone(); + self.skip_while(move |record| !types.contains(&record.r#type)) + .take_while(move |record| take_range.contains(&record.r#type)) + } + fn skip_signatures(self) -> core::iter::Filter, fn(&TlvRecord) -> bool> { self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) } } /// A slice into a [`TlvStream`] for a record. -struct TlvRecord<'a> { - r#type: u64, +pub(super) struct TlvRecord<'a> { + pub(super) r#type: u64, type_bytes: &'a [u8], // The entire TLV record. - record_bytes: &'a [u8], + pub(super) record_bytes: &'a [u8], } impl<'a> Iterator for TlvStream<'a> { @@ -212,7 +221,7 @@ impl<'a> Writeable for WithoutSignatures<'a> { #[cfg(test)] mod tests { - use super::{TlvStream, WithoutSignatures}; + use super::{SIGNATURE_TYPES, TlvStream, WithoutSignatures}; use bitcoin::hashes::{Hash, sha256}; use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; @@ -302,6 +311,38 @@ mod tests { ); } + #[test] + fn iterates_over_tlv_stream_range() { + let secp_ctx = Secp256k1::new(); + let recipient_pubkey = { + let secret_key = SecretKey::from_slice(&[41; 32]).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key() + }; + let payer_keys = { + let secret_key = SecretKey::from_slice(&[42; 32]).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key) + }; + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey) + .amount_msats(100) + .build_unchecked() + .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() + .build_unchecked() + .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys))) + .unwrap(); + + let tlv_stream = TlvStream::new(&invoice_request.bytes).range(0..1) + .chain(TlvStream::new(&invoice_request.bytes).range(1..80)) + .chain(TlvStream::new(&invoice_request.bytes).range(80..160)) + .chain(TlvStream::new(&invoice_request.bytes).range(160..240)) + .chain(TlvStream::new(&invoice_request.bytes).range(SIGNATURE_TYPES)) + .map(|r| r.record_bytes.to_vec()) + .flatten() + .collect::>(); + + assert_eq!(tlv_stream, invoice_request.bytes); + } + impl AsRef<[u8]> for InvoiceRequest { fn as_ref(&self) -> &[u8] { &self.bytes From dd2ccd232234d93c482c333b20dc71d53c4b7247 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 7 Feb 2023 19:15:44 -0600 Subject: [PATCH 06/16] 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. --- lightning/src/ln/inbound_payment.rs | 21 +++- lightning/src/offers/invoice_request.rs | 12 +- lightning/src/offers/offer.rs | 153 ++++++++++++++++++++++-- lightning/src/offers/signer.rs | 51 +++++++- lightning/src/offers/test_utils.rs | 9 ++ 5 files changed, 231 insertions(+), 15 deletions(-) diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index e6668a33cee..2d15876bf95 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -23,7 +23,7 @@ use crate::util::crypto::hkdf_extract_expand_4x; use crate::util::errors::APIError; use crate::util::logger::Logger; -use core::convert::TryInto; +use core::convert::{TryFrom, TryInto}; use core::ops::Deref; pub(crate) const IV_LEN: usize = 16; @@ -89,8 +89,8 @@ impl ExpandedKey { /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata /// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey #[allow(unused)] -#[derive(Clone, Copy)] -pub(crate) struct Nonce([u8; Self::LENGTH]); +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Nonce(pub(crate) [u8; Self::LENGTH]); impl Nonce { /// Number of bytes in the nonce. @@ -114,6 +114,21 @@ impl Nonce { } } +impl TryFrom<&[u8]> for Nonce { + type Error = (); + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != Self::LENGTH { + return Err(()); + } + + let mut copied_bytes = [0u8; Self::LENGTH]; + copied_bytes.copy_from_slice(bytes); + + Ok(Self(copied_bytes)) + } +} + enum Method { LdkPaymentHash = 0, UserPaymentHash = 1, diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 8bb5737c3f7..79dff614b68 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -54,15 +54,16 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{Message, PublicKey}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self}; use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -372,6 +373,13 @@ impl InvoiceRequest { InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } + /// Verifies that the request was for an offer created using the given key. + pub fn verify( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + self.contents.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx) + } + #[cfg(test)] fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) = diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a5935c87b8a..6a8f956ae63 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -80,8 +80,9 @@ use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::MAX_VALUE_MSAT; use crate::offers::invoice_request::InvoiceRequestBuilder; +use crate::offers::merkle::TlvStream; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; -use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -149,10 +150,11 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { /// recipient privacy by using a different signing pubkey for each offer. Otherwise, the /// provided `node_id` is used for the signing pubkey. /// - /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used to - /// verify that an [`InvoiceRequest`] was produced for the offer given an [`ExpandedKey`]. + /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used by + /// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an + /// [`ExpandedKey`]. /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_signing_pubkey( description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, @@ -566,6 +568,27 @@ impl OfferContents { self.signing_pubkey } + /// Verifies that the offer metadata was produced from the offer in the TLV stream. + pub(super) fn verify( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + match self.metadata() { + Some(metadata) => { + let tlv_stream = tlv_stream.range(OFFER_TYPES).filter(|record| { + match record.r#type { + OFFER_METADATA_TYPE => false, + OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(), + _ => true, + } + }); + signer::verify_metadata( + metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx + ) + }, + None => false, + } + } + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), @@ -653,9 +676,18 @@ impl Quantity { } } -tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, { +/// Valid type range for offer TLV records. +const OFFER_TYPES: core::ops::Range = 1..80; + +/// TLV record type for [`Offer::metadata`]. +const OFFER_METADATA_TYPE: u64 = 4; + +/// TLV record type for [`Offer::signing_pubkey`]. +const OFFER_NODE_ID_TYPE: u64 = 22; + +tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, { (2, chains: (Vec, WithoutLength)), - (4, metadata: (Vec, WithoutLength)), + (OFFER_METADATA_TYPE, metadata: (Vec, WithoutLength)), (6, currency: CurrencyCode), (8, amount: (u64, HighZeroBytesDroppedBigSize)), (10, description: (String, WithoutLength)), @@ -664,7 +696,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, { (16, paths: (Vec, WithoutLength)), (18, issuer: (String, WithoutLength)), (20, quantity_max: (u64, HighZeroBytesDroppedBigSize)), - (22, node_id: PublicKey), + (OFFER_NODE_ID_TYPE, node_id: PublicKey), }); impl Bech32Encode for Offer { @@ -751,10 +783,13 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; + use bitcoin::secp256k1::Secp256k1; use core::convert::TryFrom; use core::num::NonZeroU64; use core::time::Duration; + use crate::chain::keysinterface::KeyMaterial; use crate::ln::features::OfferFeatures; + use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::test_utils::*; @@ -865,6 +900,110 @@ mod tests { assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32])); } + #[test] + fn builds_offer_with_metadata_derived() { + let desc = "foo".to_string(); + let node_id = recipient_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder + ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + .amount_msats(1000) + .build().unwrap(); + assert_eq!(offer.signing_pubkey(), node_id); + + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(invoice_request.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered offer field + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = Some(100); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + let invoice_request = Offer::try_from(encoded_offer).unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered metadata + let mut tlv_stream = offer.as_tlv_stream(); + let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect(); + tlv_stream.metadata = Some(&metadata); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + let invoice_request = Offer::try_from(encoded_offer).unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + } + + #[test] + fn builds_offer_with_derived_signing_pubkey() { + let desc = "foo".to_string(); + let node_id = recipient_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let blinded_path = BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] }, + ], + }; + + let offer = OfferBuilder + ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + .amount_msats(1000) + .path(blinded_path) + .build().unwrap(); + assert_ne!(offer.signing_pubkey(), node_id); + + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(invoice_request.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered offer field + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = Some(100); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + let invoice_request = Offer::try_from(encoded_offer).unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered signing pubkey + let mut tlv_stream = offer.as_tlv_stream(); + let signing_pubkey = pubkey(1); + tlv_stream.node_id = Some(&signing_pubkey); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + let invoice_request = Offer::try_from(encoded_offer).unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + } + #[test] fn builds_offer_with_amount() { let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 }; diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index e1a1a4dfd6c..2ee3d13afbb 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -10,12 +10,14 @@ //! Utilities for signing offer messages and verifying metadata. use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::hashes::cmp::fixed_time_eq; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey, self}; -use core::convert::TryInto; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self}; +use core::convert::TryFrom; use core::fmt; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::offers::merkle::TlvRecord; use crate::util::ser::Writeable; use crate::prelude::*; @@ -56,7 +58,12 @@ impl Metadata { pub fn derives_keys(&self) -> bool { match self { - Metadata::Bytes(_) => false, + // Infer whether Metadata::derived_from was called on Metadata::DerivedSigningPubkey to + // produce Metadata::Bytes. This is merely to determine which fields should be included + // when verifying a message. It doesn't necessarily indicate that keys were in fact + // derived, as wouldn't be the case if a Metadata::Bytes with length Nonce::LENGTH had + // been set explicitly. + Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -148,3 +155,41 @@ impl MetadataMaterial { (self.nonce.as_slice().to_vec(), keys) } } + +/// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: +/// - a 128-bit [`Nonce`] and possibly +/// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`]. +/// +/// If the latter is not included in the metadata, the TLV stream is used to check if the given +/// `signing_pubkey` can be derived from it. +pub(super) fn verify_metadata<'a, T: secp256k1::Signing>( + metadata: &Vec, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, + secp_ctx: &Secp256k1 +) -> bool { + if metadata.len() < Nonce::LENGTH { + return false; + } + + let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) { + Ok(nonce) => nonce, + Err(_) => return false, + }; + let mut hmac = expanded_key.hmac_for_offer(nonce, iv_bytes); + + for record in tlv_stream { + hmac.input(record.record_bytes); + } + + if metadata.len() == Nonce::LENGTH { + hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); + let hmac = Hmac::from_engine(hmac); + let derived_pubkey = SecretKey::from_slice(hmac.as_inner()).unwrap().public_key(&secp_ctx); + fixed_time_eq(&signing_pubkey.serialize(), &derived_pubkey.serialize()) + } else if metadata[Nonce::LENGTH..].len() == Sha256::LEN { + hmac.input(DERIVED_METADATA_HMAC_INPUT); + fixed_time_eq(&metadata[Nonce::LENGTH..], &Hmac::from_engine(hmac).into_inner()) + } else { + false + } +} diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs index 7447b86fb84..43664079dbd 100644 --- a/lightning/src/offers/test_utils.rs +++ b/lightning/src/offers/test_utils.rs @@ -13,6 +13,7 @@ use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::Infallible; use core::time::Duration; +use crate::chain::keysinterface::EntropySource; use crate::ln::PaymentHash; use crate::ln::features::BlindedHopFeatures; use crate::offers::invoice::BlindedPayInfo; @@ -108,3 +109,11 @@ pub(super) fn now() -> Duration { .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") } + +pub(super) struct FixedEntropy; + +impl EntropySource for FixedEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [42; 32] + } +} From e15044b8899863654f2659611b8f45703b656b32 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 10 Mar 2023 17:12:12 -0600 Subject: [PATCH 07/16] Refactor InvoiceRequestContents fields into a sub-struct InvoiceRequestBuilder has a field containing InvoiceRequestContents. When deriving the payer_id from the remaining fields, a struct is needed without payer_id as it not optional. Refactor InvoiceRequestContents to have an inner struct without the payer_id such that InvoiceRequestBuilder can use it instead. --- lightning/src/offers/invoice.rs | 7 ++- lightning/src/offers/invoice_request.rs | 81 ++++++++++++++++--------- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index b0783a306e5..9d83ce899b1 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -146,7 +146,7 @@ impl<'a> InvoiceBuilder<'a> { ) -> Result { let amount_msats = match invoice_request.amount_msats() { Some(amount_msats) => amount_msats, - None => match invoice_request.contents.offer.amount() { + None => match invoice_request.contents.inner.offer.amount() { Some(Amount::Bitcoin { amount_msats }) => { amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) .ok_or(SemanticError::InvalidAmount)? @@ -161,7 +161,7 @@ impl<'a> InvoiceBuilder<'a> { fields: InvoiceFields { payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, fallbacks: None, features: Bolt12InvoiceFeatures::empty(), - signing_pubkey: invoice_request.contents.offer.signing_pubkey(), + signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(), }, }; @@ -493,7 +493,8 @@ impl InvoiceContents { #[cfg(feature = "std")] fn is_offer_or_refund_expired(&self) -> bool { match self { - InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(), + InvoiceContents::ForOffer { invoice_request, .. } => + invoice_request.inner.offer.is_expired(), InvoiceContents::ForRefund { refund, .. } => refund.is_expired(), } } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 79dff614b68..124ecc95c7a 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -90,9 +90,12 @@ impl<'a> InvoiceRequestBuilder<'a> { Self { offer, invoice_request: InvoiceRequestContents { - payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None, - amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, - payer_id, payer_note: None, + inner: InvoiceRequestContentsWithoutPayerId { + payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None, + amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, + payer_note: None, + }, + payer_id, }, } } @@ -108,7 +111,7 @@ impl<'a> InvoiceRequestBuilder<'a> { return Err(SemanticError::UnsupportedChain); } - self.invoice_request.chain = Some(chain); + self.invoice_request.inner.chain = Some(chain); Ok(self) } @@ -119,10 +122,10 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// [`quantity`]: Self::quantity pub fn amount_msats(mut self, amount_msats: u64) -> Result { - self.invoice_request.offer.check_amount_msats_for_quantity( - Some(amount_msats), self.invoice_request.quantity + self.invoice_request.inner.offer.check_amount_msats_for_quantity( + Some(amount_msats), self.invoice_request.inner.quantity )?; - self.invoice_request.amount_msats = Some(amount_msats); + self.invoice_request.inner.amount_msats = Some(amount_msats); Ok(self) } @@ -131,8 +134,8 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// Successive calls to this method will override the previous setting. pub fn quantity(mut self, quantity: u64) -> Result { - self.invoice_request.offer.check_quantity(Some(quantity))?; - self.invoice_request.quantity = Some(quantity); + self.invoice_request.inner.offer.check_quantity(Some(quantity))?; + self.invoice_request.inner.quantity = Some(quantity); Ok(self) } @@ -140,7 +143,7 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// Successive calls to this method will override the previous setting. pub fn payer_note(mut self, payer_note: String) -> Self { - self.invoice_request.payer_note = Some(payer_note); + self.invoice_request.inner.payer_note = Some(payer_note); self } @@ -159,16 +162,16 @@ impl<'a> InvoiceRequestBuilder<'a> { } if chain == self.offer.implied_chain() { - self.invoice_request.chain = None; + self.invoice_request.inner.chain = None; } - if self.offer.amount().is_none() && self.invoice_request.amount_msats.is_none() { + if self.offer.amount().is_none() && self.invoice_request.inner.amount_msats.is_none() { return Err(SemanticError::MissingAmount); } - self.invoice_request.offer.check_quantity(self.invoice_request.quantity)?; - self.invoice_request.offer.check_amount_msats_for_quantity( - self.invoice_request.amount_msats, self.invoice_request.quantity + self.invoice_request.inner.offer.check_quantity(self.invoice_request.inner.quantity)?; + self.invoice_request.inner.offer.check_amount_msats_for_quantity( + self.invoice_request.inner.amount_msats, self.invoice_request.inner.quantity )?; let InvoiceRequestBuilder { offer, invoice_request } = self; @@ -180,22 +183,22 @@ impl<'a> InvoiceRequestBuilder<'a> { impl<'a> InvoiceRequestBuilder<'a> { fn chain_unchecked(mut self, network: Network) -> Self { let chain = ChainHash::using_genesis_block(network); - self.invoice_request.chain = Some(chain); + self.invoice_request.inner.chain = Some(chain); self } fn amount_msats_unchecked(mut self, amount_msats: u64) -> Self { - self.invoice_request.amount_msats = Some(amount_msats); + self.invoice_request.inner.amount_msats = Some(amount_msats); self } fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { - self.invoice_request.features = features; + self.invoice_request.inner.features = features; self } fn quantity_unchecked(mut self, quantity: u64) -> Self { - self.invoice_request.quantity = Some(quantity); + self.invoice_request.inner.quantity = Some(quantity); self } @@ -265,13 +268,19 @@ pub struct InvoiceRequest { #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] pub(super) struct InvoiceRequestContents { + pub(super) inner: InvoiceRequestContentsWithoutPayerId, + payer_id: PublicKey, +} + +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub(super) struct InvoiceRequestContentsWithoutPayerId { payer: PayerContents, pub(super) offer: OfferContents, chain: Option, amount_msats: Option, features: InvoiceRequestFeatures, quantity: Option, - payer_id: PublicKey, payer_note: Option, } @@ -281,7 +290,7 @@ impl InvoiceRequest { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0[..] + &self.contents.inner.payer.0[..] } /// A chain from [`Offer::chains`] that the offer is valid for. @@ -294,17 +303,17 @@ impl InvoiceRequest { /// /// [`chain`]: Self::chain pub fn amount_msats(&self) -> Option { - self.contents.amount_msats + self.contents.inner.amount_msats } /// Features pertaining to requesting an invoice. pub fn features(&self) -> &InvoiceRequestFeatures { - &self.contents.features + &self.contents.inner.features } /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. pub fn quantity(&self) -> Option { - self.contents.quantity + self.contents.inner.quantity } /// A possibly transient pubkey used to sign the invoice request. @@ -315,7 +324,8 @@ impl InvoiceRequest { /// A payer-provided note which will be seen by the recipient and reflected back in the invoice /// response. pub fn payer_note(&self) -> Option { - self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) + self.contents.inner.payer_note.as_ref() + .map(|payer_note| PrintableString(payer_note.as_str())) } /// Signature of the invoice request using [`payer_id`]. @@ -377,7 +387,7 @@ impl InvoiceRequest { pub fn verify( &self, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> bool { - self.contents.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx) + self.contents.inner.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx) } #[cfg(test)] @@ -392,6 +402,18 @@ impl InvoiceRequest { } impl InvoiceRequestContents { + pub(super) fn chain(&self) -> ChainHash { + self.inner.chain() + } + + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { + let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream(); + invoice_request.payer_id = Some(&self.payer_id); + (payer, offer, invoice_request) + } +} + +impl InvoiceRequestContentsWithoutPayerId { pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } @@ -413,7 +435,7 @@ impl InvoiceRequestContents { amount: self.amount_msats, features, quantity: self.quantity, - payer_id: Some(&self.payer_id), + payer_id: None, payer_note: self.payer_note.as_ref(), }; @@ -531,7 +553,10 @@ impl TryFrom for InvoiceRequestContents { }; Ok(InvoiceRequestContents { - payer, offer, chain, amount_msats: amount, features, quantity, payer_id, payer_note, + inner: InvoiceRequestContentsWithoutPayerId { + payer, offer, chain, amount_msats: amount, features, quantity, payer_note, + }, + payer_id, }) } } From 3880e69237d7f6a33db1912176de5a745ea99b41 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 30 Jan 2023 14:56:42 -0600 Subject: [PATCH 08/16] InvoiceRequest metadata and payer id derivation Add support for deriving a transient payer id for each InvoiceRequest from an ExpandedKey and a nonce. This facilitates payer privacy by not tying any InvoiceRequest to any other nor to the payer's node id. Additionally, support stateless Invoice verification by setting payer metadata using an HMAC over the nonce and the remaining TLV records, which will be later verified when receiving an Invoice response. --- lightning/src/offers/invoice_request.rs | 216 +++++++++++++++++++----- lightning/src/offers/offer.rs | 49 +++++- lightning/src/offers/payer.rs | 3 +- lightning/src/offers/refund.rs | 8 +- lightning/src/offers/signer.rs | 6 + 5 files changed, 234 insertions(+), 48 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 124ecc95c7a..26151dfbd18 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -54,19 +54,22 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; -use core::convert::TryFrom; +use core::convert::{Infallible, TryFrom}; +use core::ops::Deref; +use crate::chain::keysinterface::EntropySource; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; -use crate::ln::inbound_payment::ExpandedKey; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self}; use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -75,28 +78,83 @@ use crate::prelude::*; const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); +const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; + /// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow. /// /// See [module-level documentation] for usage. /// /// [module-level documentation]: self -pub struct InvoiceRequestBuilder<'a> { +pub struct InvoiceRequestBuilder<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> { offer: &'a Offer, - invoice_request: InvoiceRequestContents, + invoice_request: InvoiceRequestContentsWithoutPayerId, + payer_id: Option, + payer_id_strategy: core::marker::PhantomData

, + secp_ctx: Option<&'b Secp256k1>, } -impl<'a> InvoiceRequestBuilder<'a> { +/// Indicates how [`InvoiceRequest::payer_id`] will be set. +pub trait PayerIdStrategy {} + +/// [`InvoiceRequest::payer_id`] will be explicitly set. +pub struct ExplicitPayerId {} + +/// [`InvoiceRequest::payer_id`] will be derived. +pub struct DerivedPayerId {} + +impl PayerIdStrategy for ExplicitPayerId {} +impl PayerIdStrategy for DerivedPayerId {} + +impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> { pub(super) fn new(offer: &'a Offer, metadata: Vec, payer_id: PublicKey) -> Self { Self { offer, - invoice_request: InvoiceRequestContents { - inner: InvoiceRequestContentsWithoutPayerId { - payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None, - amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, - payer_note: None, - }, - payer_id, - }, + invoice_request: Self::create_contents(offer, Metadata::Bytes(metadata)), + payer_id: Some(payer_id), + payer_id_strategy: core::marker::PhantomData, + secp_ctx: None, + } + } + + pub(super) fn deriving_metadata( + offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES + ) -> Self where ES::Target: EntropySource { + let nonce = Nonce::from_entropy_source(entropy_source); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let metadata = Metadata::Derived(derivation_material); + Self { + offer, + invoice_request: Self::create_contents(offer, metadata), + payer_id: Some(payer_id), + payer_id_strategy: core::marker::PhantomData, + secp_ctx: None, + } + } +} + +impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> { + pub(super) fn deriving_payer_id( + offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1 + ) -> Self where ES::Target: EntropySource { + let nonce = Nonce::from_entropy_source(entropy_source); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let metadata = Metadata::DerivedSigningPubkey(derivation_material); + Self { + offer, + invoice_request: Self::create_contents(offer, metadata), + payer_id: None, + payer_id_strategy: core::marker::PhantomData, + secp_ctx: Some(secp_ctx), + } + } +} + +impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, P, T> { + fn create_contents(offer: &Offer, metadata: Metadata) -> InvoiceRequestContentsWithoutPayerId { + let offer = offer.contents.clone(); + InvoiceRequestContentsWithoutPayerId { + payer: PayerContents(metadata), offer, chain: None, amount_msats: None, + features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None, } } @@ -111,7 +169,7 @@ impl<'a> InvoiceRequestBuilder<'a> { return Err(SemanticError::UnsupportedChain); } - self.invoice_request.inner.chain = Some(chain); + self.invoice_request.chain = Some(chain); Ok(self) } @@ -122,10 +180,10 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// [`quantity`]: Self::quantity pub fn amount_msats(mut self, amount_msats: u64) -> Result { - self.invoice_request.inner.offer.check_amount_msats_for_quantity( - Some(amount_msats), self.invoice_request.inner.quantity + self.invoice_request.offer.check_amount_msats_for_quantity( + Some(amount_msats), self.invoice_request.quantity )?; - self.invoice_request.inner.amount_msats = Some(amount_msats); + self.invoice_request.amount_msats = Some(amount_msats); Ok(self) } @@ -134,8 +192,8 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// Successive calls to this method will override the previous setting. pub fn quantity(mut self, quantity: u64) -> Result { - self.invoice_request.inner.offer.check_quantity(Some(quantity))?; - self.invoice_request.inner.quantity = Some(quantity); + self.invoice_request.offer.check_quantity(Some(quantity))?; + self.invoice_request.quantity = Some(quantity); Ok(self) } @@ -143,13 +201,14 @@ impl<'a> InvoiceRequestBuilder<'a> { /// /// Successive calls to this method will override the previous setting. pub fn payer_note(mut self, payer_note: String) -> Self { - self.invoice_request.inner.payer_note = Some(payer_note); + self.invoice_request.payer_note = Some(payer_note); self } - /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed - /// by [`UnsignedInvoiceRequest::sign`]. - pub fn build(mut self) -> Result, SemanticError> { + fn build_with_checks(mut self) -> Result< + (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>), + SemanticError + > { #[cfg(feature = "std")] { if self.offer.is_expired() { return Err(SemanticError::AlreadyExpired); @@ -162,49 +221,114 @@ impl<'a> InvoiceRequestBuilder<'a> { } if chain == self.offer.implied_chain() { - self.invoice_request.inner.chain = None; + self.invoice_request.chain = None; } - if self.offer.amount().is_none() && self.invoice_request.inner.amount_msats.is_none() { + if self.offer.amount().is_none() && self.invoice_request.amount_msats.is_none() { return Err(SemanticError::MissingAmount); } - self.invoice_request.inner.offer.check_quantity(self.invoice_request.inner.quantity)?; - self.invoice_request.inner.offer.check_amount_msats_for_quantity( - self.invoice_request.inner.amount_msats, self.invoice_request.inner.quantity + self.invoice_request.offer.check_quantity(self.invoice_request.quantity)?; + self.invoice_request.offer.check_amount_msats_for_quantity( + self.invoice_request.amount_msats, self.invoice_request.quantity )?; - let InvoiceRequestBuilder { offer, invoice_request } = self; - Ok(UnsignedInvoiceRequest { offer, invoice_request }) + Ok(self.build_without_checks()) + } + + fn build_without_checks(mut self) -> + (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>) + { + // Create the metadata for stateless verification of an Invoice. + let mut keys = None; + let secp_ctx = self.secp_ctx.clone(); + if self.invoice_request.payer.0.has_derivation_material() { + let mut metadata = core::mem::take(&mut self.invoice_request.payer.0); + + let mut tlv_stream = self.invoice_request.as_tlv_stream(); + debug_assert!(tlv_stream.2.payer_id.is_none()); + tlv_stream.0.metadata = None; + if !metadata.derives_keys() { + tlv_stream.2.payer_id = self.payer_id.as_ref(); + } + + let (derived_metadata, derived_keys) = metadata.derive_from(tlv_stream, self.secp_ctx); + metadata = derived_metadata; + keys = derived_keys; + if let Some(keys) = keys { + debug_assert!(self.payer_id.is_none()); + self.payer_id = Some(keys.public_key()); + } + + self.invoice_request.payer.0 = metadata; + } + + debug_assert!(self.invoice_request.payer.0.as_bytes().is_some()); + debug_assert!(self.payer_id.is_some()); + let payer_id = self.payer_id.unwrap(); + + let unsigned_invoice = UnsignedInvoiceRequest { + offer: self.offer, + invoice_request: InvoiceRequestContents { + inner: self.invoice_request, + payer_id, + }, + }; + + (unsigned_invoice, keys, secp_ctx) + } +} + +impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> { + /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed + /// by [`UnsignedInvoiceRequest::sign`]. + pub fn build(self) -> Result, SemanticError> { + let (unsigned_invoice_request, keys, _) = self.build_with_checks()?; + debug_assert!(keys.is_none()); + Ok(unsigned_invoice_request) + } +} + +impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> { + /// Builds a signed [`InvoiceRequest`] after checking for valid semantics. + pub fn build_and_sign(self) -> Result { + let (unsigned_invoice_request, keys, secp_ctx) = self.build_with_checks()?; + debug_assert!(keys.is_some()); + + let secp_ctx = secp_ctx.unwrap(); + let keys = keys.unwrap(); + let invoice_request = unsigned_invoice_request + .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .unwrap(); + Ok(invoice_request) } } #[cfg(test)] -impl<'a> InvoiceRequestBuilder<'a> { +impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, P, T> { fn chain_unchecked(mut self, network: Network) -> Self { let chain = ChainHash::using_genesis_block(network); - self.invoice_request.inner.chain = Some(chain); + self.invoice_request.chain = Some(chain); self } fn amount_msats_unchecked(mut self, amount_msats: u64) -> Self { - self.invoice_request.inner.amount_msats = Some(amount_msats); + self.invoice_request.amount_msats = Some(amount_msats); self } fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { - self.invoice_request.inner.features = features; + self.invoice_request.features = features; self } fn quantity_unchecked(mut self, quantity: u64) -> Self { - self.invoice_request.inner.quantity = Some(quantity); + self.invoice_request.quantity = Some(quantity); self } pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { - let InvoiceRequestBuilder { offer, invoice_request } = self; - UnsignedInvoiceRequest { offer, invoice_request } + self.build_without_checks().0 } } @@ -290,7 +414,7 @@ impl InvoiceRequest { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.inner.payer.0[..] + self.contents.metadata() } /// A chain from [`Offer::chains`] that the offer is valid for. @@ -402,6 +526,10 @@ impl InvoiceRequest { } impl InvoiceRequestContents { + pub fn metadata(&self) -> &[u8] { + self.inner.metadata() + } + pub(super) fn chain(&self) -> ChainHash { self.inner.chain() } @@ -414,13 +542,17 @@ impl InvoiceRequestContents { } impl InvoiceRequestContentsWithoutPayerId { + pub(super) fn metadata(&self) -> &[u8] { + self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) + } + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let payer = PayerTlvStreamRef { - metadata: Some(&self.payer.0), + metadata: self.payer.0.as_bytes(), }; let offer = self.offer.as_tlv_stream(); @@ -530,7 +662,7 @@ impl TryFrom for InvoiceRequestContents { let payer = match metadata { None => return Err(SemanticError::MissingPayerMetadata), - Some(metadata) => PayerContents(metadata), + Some(metadata) => PayerContents(Metadata::Bytes(metadata)), }; let offer = OfferContents::try_from(offer_tlv_stream)?; @@ -1038,7 +1170,7 @@ mod tests { let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) .amount_msats(1000) .build().unwrap() - .request_invoice(vec![42; 32], payer_pubkey()).unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 6a8f956ae63..468018d55ec 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -79,7 +79,7 @@ use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::MAX_VALUE_MSAT; -use crate::offers::invoice_request::InvoiceRequestBuilder; +use crate::offers::invoice_request::{DerivedPayerId, ExplicitPayerId, InvoiceRequestBuilder}; use crate::offers::merkle::TlvStream; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; @@ -439,6 +439,51 @@ impl Offer { self.contents.signing_pubkey() } + /// Similar to [`Offer::request_invoice`] except it: + /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each + /// request, and + /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such + /// that it can be used to determine if the invoice was requested using a base [`ExpandedKey`] + /// from which the payer id was derived. + /// + /// Useful to protect the sender's privacy. + /// + /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id + /// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata + /// [`Invoice::verify`]: crate::offers::invoice::Invoice::verify + /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey + pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>( + &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1 + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx)) + } + + /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the + /// [`InvoiceRequest::payer_id`] instead of deriving a different key for each request. + /// + /// Useful for recurring payments using the same `payer_id` with different invoices. + /// + /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id + pub fn request_invoice_deriving_metadata( + &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source)) + } + /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which /// will be reflected in the `Invoice` response. /// @@ -454,7 +499,7 @@ impl Offer { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub fn request_invoice( &self, metadata: Vec, payer_id: PublicKey - ) -> Result { + ) -> Result, SemanticError> { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 12b471c6ce4..7609c466619 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -9,6 +9,7 @@ //! Data structures and encoding for `invoice_request_metadata` records. +use crate::offers::signer::Metadata; use crate::util::ser::WithoutLength; use crate::prelude::*; @@ -19,7 +20,7 @@ use crate::prelude::*; /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] -pub(super) struct PayerContents(pub Vec); +pub(super) struct PayerContents(pub Metadata); tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { (0, metadata: (Vec, WithoutLength)), diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 6d44eb3da6d..999d68c448c 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -86,6 +86,7 @@ use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvS use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::Metadata; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -117,6 +118,7 @@ impl RefundBuilder { return Err(SemanticError::InvalidAmount); } + let metadata = Metadata::Bytes(metadata); let refund = RefundContents { payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), @@ -281,7 +283,7 @@ impl Refund { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0 + &self.contents.payer.0.as_bytes().unwrap()[..] } /// A chain that the refund is valid for. @@ -405,7 +407,7 @@ impl RefundContents { pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { let payer = PayerTlvStreamRef { - metadata: Some(&self.payer.0), + metadata: self.payer.0.as_bytes(), }; let offer = OfferTlvStreamRef { @@ -509,7 +511,7 @@ impl TryFrom for RefundContents { let payer = match payer_metadata { None => return Err(SemanticError::MissingPayerMetadata), - Some(metadata) => PayerContents(metadata), + Some(metadata) => PayerContents(Metadata::Bytes(metadata)), }; if metadata.is_some() { diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 2ee3d13afbb..a8ea941e3be 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -96,6 +96,12 @@ impl Metadata { } } +impl Default for Metadata { + fn default() -> Self { + Metadata::Bytes(vec![]) + } +} + impl fmt::Debug for Metadata { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { From 9bd43e077fd00add0491960aeb5533a75d9d71d3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 20 Mar 2023 20:24:54 -0500 Subject: [PATCH 09/16] Fix builder docs in offers module --- lightning/src/offers/invoice_request.rs | 6 ++---- lightning/src/offers/offer.rs | 4 ++-- lightning/src/offers/refund.rs | 6 ++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 26151dfbd18..f617383fdcf 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -459,13 +459,12 @@ impl InvoiceRequest { self.signature } - /// Creates an [`Invoice`] for the request with the given required fields and using the + /// Creates an [`InvoiceBuilder`] for the request with the given required fields and using the /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. /// /// See [`InvoiceRequest::respond_with_no_std`] for further details where the aforementioned /// creation time is used for the `created_at` parameter. /// - /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Duration`]: core::time::Duration #[cfg(feature = "std")] pub fn respond_with( @@ -478,7 +477,7 @@ impl InvoiceRequest { self.respond_with_no_std(payment_paths, payment_hash, created_at) } - /// Creates an [`Invoice`] for the request with the given required fields. + /// Creates an [`InvoiceBuilder`] for the request with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where @@ -494,7 +493,6 @@ impl InvoiceRequest { /// /// Errors if the request contains unknown required features. /// - /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 468018d55ec..617496d4b51 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -484,8 +484,8 @@ impl Offer { Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source)) } - /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which - /// will be reflected in the `Invoice` response. + /// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`, + /// which will be reflected in the `Invoice` response. /// /// The `metadata` is useful for including information about the derivation of `payer_id` such /// that invoice response handling can be stateless. Also serves as payer-provided entropy while diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 999d68c448c..4628c334fbe 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -321,13 +321,12 @@ impl Refund { self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } - /// Creates an [`Invoice`] for the refund with the given required fields and using the + /// Creates an [`InvoiceBuilder`] for the refund with the given required fields and using the /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. /// /// See [`Refund::respond_with_no_std`] for further details where the aforementioned creation /// time is used for the `created_at` parameter. /// - /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Duration`]: core::time::Duration #[cfg(feature = "std")] pub fn respond_with( @@ -341,7 +340,7 @@ impl Refund { self.respond_with_no_std(payment_paths, payment_hash, signing_pubkey, created_at) } - /// Creates an [`Invoice`] for the refund with the given required fields. + /// Creates an [`InvoiceBuilder`] for the refund with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where @@ -360,7 +359,6 @@ impl Refund { /// /// Errors if the request contains unknown required features. /// - /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, From 022eadc4dbf0f60179674f936d604cade6c5dd9e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 30 Jan 2023 14:57:43 -0600 Subject: [PATCH 10/16] Stateless verification of Invoice for Offer Verify that an Invoice was produced from an InvoiceRequest constructed by the payer using the payer metadata reflected in the Invoice. The payer metadata consists of a 128-bit encrypted nonce and possibly a 256-bit HMAC over the nonce and InvoiceRequest TLV records (excluding the payer id) using an ExpandedKey. Thus, the HMAC can be reproduced from the invoice request 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 payer id. --- lightning/src/offers/invoice.rs | 27 +++- lightning/src/offers/invoice_request.rs | 179 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 1 + lightning/src/offers/offer.rs | 6 +- lightning/src/offers/payer.rs | 8 +- lightning/src/offers/signer.rs | 2 +- 6 files changed, 209 insertions(+), 14 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 9d83ce899b1..f5a613aca65 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -97,7 +97,7 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{Message, PublicKey}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; @@ -106,9 +106,10 @@ use core::time::Duration; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self}; use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef}; @@ -123,7 +124,7 @@ use std::time::SystemTime; const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); -const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); +pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); /// Builds an [`Invoice`] from either: /// - an [`InvoiceRequest`] for the "offer to be paid" flow or @@ -476,8 +477,15 @@ impl Invoice { merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone() } + /// Verifies that the invoice was for a request or refund created using the given key. + pub fn verify( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx) + } + #[cfg(test)] - fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { + pub(super) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) = self.contents.as_tlv_stream(); let signature_tlv_stream = SignatureTlvStreamRef { @@ -520,6 +528,17 @@ impl InvoiceContents { } } + fn verify( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + invoice_request.verify(tlv_stream, key, secp_ctx) + }, + _ => todo!(), + } + } + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { let (payer, offer, invoice_request) = match self { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index f617383fdcf..a7cdbfc0f15 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -66,10 +66,10 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self}; -use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{OFFER_TYPES, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -532,6 +532,22 @@ impl InvoiceRequestContents { self.inner.chain() } + /// Verifies that the payer metadata was produced from the invoice request in the TLV stream. + pub(super) fn verify( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + let offer_records = tlv_stream.clone().range(OFFER_TYPES); + let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { + match record.r#type { + PAYER_METADATA_TYPE => false, // Should be outside range + INVOICE_REQUEST_PAYER_ID_TYPE => !self.inner.payer.0.derives_keys(), + _ => true, + } + }); + let tlv_stream = offer_records.chain(invreq_records); + signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx) + } + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream(); invoice_request.payer_id = Some(&self.payer_id); @@ -585,12 +601,20 @@ impl Writeable for InvoiceRequestContents { } } -tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { +/// Valid type range for invoice_request TLV records. +const INVOICE_REQUEST_TYPES: core::ops::Range = 80..160; + +/// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`]. +/// +/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id +const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88; + +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, { (80, chain: ChainHash), (82, amount: (u64, HighZeroBytesDroppedBigSize)), (84, features: (InvoiceRequestFeatures, WithoutLength)), (86, quantity: (u64, HighZeroBytesDroppedBigSize)), - (88, payer_id: PublicKey), + (INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey), (89, payer_note: (String, WithoutLength)), }); @@ -702,8 +726,11 @@ mod tests { use core::num::NonZeroU64; #[cfg(feature = "std")] use core::time::Duration; + use crate::chain::keysinterface::KeyMaterial; use crate::ln::features::InvoiceRequestFeatures; + use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::invoice::{Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; @@ -800,6 +827,148 @@ mod tests { } } + #[test] + fn builds_invoice_request_with_derived_metadata() { + let payer_id = payer_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let invoice_request = offer + .request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy) + .unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert_eq!(invoice_request.payer_id(), payer_pubkey()); + + let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered fields + let ( + payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, + mut invoice_tlv_stream, mut signature_tlv_stream + ) = invoice.as_tlv_stream(); + invoice_request_tlv_stream.amount = Some(2000); + invoice_tlv_stream.amount = Some(2000); + + let tlv_stream = + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); + let mut bytes = Vec::new(); + tlv_stream.write(&mut bytes).unwrap(); + + let signature = merkle::sign_message( + recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() + ).unwrap(); + signature_tlv_stream.signature = Some(&signature); + + let mut encoded_invoice = bytes; + signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + + let invoice = Invoice::try_from(encoded_invoice).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered metadata + let ( + mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + mut signature_tlv_stream + ) = invoice.as_tlv_stream(); + let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect(); + payer_tlv_stream.metadata = Some(&metadata); + + let tlv_stream = + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); + let mut bytes = Vec::new(); + tlv_stream.write(&mut bytes).unwrap(); + + let signature = merkle::sign_message( + recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() + ).unwrap(); + signature_tlv_stream.signature = Some(&signature); + + let mut encoded_invoice = bytes; + signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + + let invoice = Invoice::try_from(encoded_invoice).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + } + + #[test] + fn builds_invoice_request_with_derived_payer_id() { + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let invoice_request = offer + .request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx) + .unwrap() + .build_and_sign() + .unwrap(); + + let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered fields + let ( + payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, + mut invoice_tlv_stream, mut signature_tlv_stream + ) = invoice.as_tlv_stream(); + invoice_request_tlv_stream.amount = Some(2000); + invoice_tlv_stream.amount = Some(2000); + + let tlv_stream = + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); + let mut bytes = Vec::new(); + tlv_stream.write(&mut bytes).unwrap(); + + let signature = merkle::sign_message( + recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() + ).unwrap(); + signature_tlv_stream.signature = Some(&signature); + + let mut encoded_invoice = bytes; + signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + + let invoice = Invoice::try_from(encoded_invoice).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered payer id + let ( + payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream, + mut signature_tlv_stream + ) = invoice.as_tlv_stream(); + let payer_id = pubkey(1); + invoice_request_tlv_stream.payer_id = Some(&payer_id); + + let tlv_stream = + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); + let mut bytes = Vec::new(); + tlv_stream.write(&mut bytes).unwrap(); + + let signature = merkle::sign_message( + recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() + ).unwrap(); + signature_tlv_stream.signature = Some(&signature); + + let mut encoded_invoice = bytes; + signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + + let invoice = Invoice::try_from(encoded_invoice).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + } + #[test] fn builds_invoice_request_with_chain() { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index f6827467420..3b05899a8f5 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -143,6 +143,7 @@ fn tagged_branch_hash_from_engine( /// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a /// well-formed TLV stream. +#[derive(Clone)] pub(super) struct TlvStream<'a> { data: io::Cursor<&'a [u8]>, } diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 617496d4b51..9f22e9af184 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -443,8 +443,8 @@ impl Offer { /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each /// request, and /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such - /// that it can be used to determine if the invoice was requested using a base [`ExpandedKey`] - /// from which the payer id was derived. + /// that it can be used by [`Invoice::verify`] to determine if the invoice was requested using + /// a base [`ExpandedKey`] from which the payer id was derived. /// /// Useful to protect the sender's privacy. /// @@ -722,7 +722,7 @@ impl Quantity { } /// Valid type range for offer TLV records. -const OFFER_TYPES: core::ops::Range = 1..80; +pub(super) const OFFER_TYPES: core::ops::Range = 1..80; /// TLV record type for [`Offer::metadata`]. const OFFER_METADATA_TYPE: u64 = 4; diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 7609c466619..bfc02b5dbcb 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -22,6 +22,12 @@ use crate::prelude::*; #[cfg_attr(test, derive(PartialEq))] pub(super) struct PayerContents(pub Metadata); +/// TLV record type for [`InvoiceRequest::metadata`] and [`Refund::metadata`]. +/// +/// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata +/// [`Refund::metadata`]: crate::offers::refund::Refund::metadata +pub(super) const PAYER_METADATA_TYPE: u64 = 0; + tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { - (0, metadata: (Vec, WithoutLength)), + (PAYER_METADATA_TYPE, metadata: (Vec, WithoutLength)), }); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index a8ea941e3be..f6141e59699 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -169,7 +169,7 @@ impl MetadataMaterial { /// If the latter is not included in the metadata, the TLV stream is used to check if the given /// `signing_pubkey` can be derived from it. pub(super) fn verify_metadata<'a, T: secp256k1::Signing>( - metadata: &Vec, expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, secp_ctx: &Secp256k1 ) -> bool { From 1a7540f2c936ec778e76050f22371829dbfa9255 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 6 Feb 2023 15:10:07 -0600 Subject: [PATCH 11/16] Refund metadata and payer id derivation Add support for deriving a transient payer id for each Refund from an ExpandedKey and a nonce. This facilitates payer privacy by not tying any Refund to any other nor to the payer's node id. Additionally, support stateless Invoice verification by setting payer metadata using an HMAC over the nonce and the remaining TLV records, which will be later verified when receiving an Invoice response. --- lightning/src/offers/refund.rs | 92 ++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 4628c334fbe..b87febc7f6d 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -73,20 +73,23 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{PublicKey, Secp256k1, self}; use core::convert::TryFrom; +use core::ops::Deref; use core::str::FromStr; use core::time::Duration; +use crate::chain::keysinterface::EntropySource; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::Metadata; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -96,16 +99,19 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~"; + /// Builds a [`Refund`] for the "offer for money" flow. /// /// See [module-level documentation] for usage. /// /// [module-level documentation]: self -pub struct RefundBuilder { +pub struct RefundBuilder<'a, T: secp256k1::Signing> { refund: RefundContents, + secp_ctx: Option<&'a Secp256k1>, } -impl RefundBuilder { +impl<'a> RefundBuilder<'a, secp256k1::SignOnly> { /// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to /// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey. /// @@ -119,13 +125,47 @@ impl RefundBuilder { } let metadata = Metadata::Bytes(metadata); - let refund = RefundContents { - payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), - quantity: None, payer_id, payer_note: None, - }; + Ok(Self { + refund: RefundContents { + payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, + paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id, payer_note: None, + }, + secp_ctx: None, + }) + } +} - Ok(RefundBuilder { refund }) +impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { + /// Similar to [`RefundBuilder::new`] except, if [`RefundBuilder::path`] is called, the payer id + /// is derived from the given [`ExpandedKey`] and nonce. This provides sender privacy by using a + /// different payer id for each refund, assuming a different nonce is used. Otherwise, the + /// provided `node_id` is used for the payer id. + /// + /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to + /// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey + pub fn deriving_payer_id( + description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + secp_ctx: &'a Secp256k1, amount_msats: u64 + ) -> Result where ES::Target: EntropySource { + if amount_msats > MAX_VALUE_MSAT { + return Err(SemanticError::InvalidAmount); + } + + let nonce = Nonce::from_entropy_source(entropy_source); + let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); + let metadata = Metadata::DerivedSigningPubkey(derivation_material); + Ok(Self { + refund: RefundContents { + payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, + paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id: node_id, payer_note: None, + }, + secp_ctx: Some(secp_ctx), + }) } /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has @@ -192,18 +232,38 @@ impl RefundBuilder { self.refund.chain = None; } + // Create the metadata for stateless verification of an Invoice. + if self.refund.payer.0.has_derivation_material() { + let mut metadata = core::mem::take(&mut self.refund.payer.0); + + if self.refund.paths.is_none() { + metadata = metadata.without_keys(); + } + + let mut tlv_stream = self.refund.as_tlv_stream(); + tlv_stream.0.metadata = None; + if metadata.derives_keys() { + tlv_stream.2.payer_id = None; + } + + let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx); + metadata = derived_metadata; + if let Some(keys) = keys { + self.refund.payer_id = keys.public_key(); + } + + self.refund.payer.0 = metadata; + } + let mut bytes = Vec::new(); self.refund.write(&mut bytes).unwrap(); - Ok(Refund { - bytes, - contents: self.refund, - }) + Ok(Refund { bytes, contents: self.refund }) } } #[cfg(test)] -impl RefundBuilder { +impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> { fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { self.refund.features = features; self @@ -283,7 +343,7 @@ impl Refund { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0.as_bytes().unwrap()[..] + self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) } /// A chain that the refund is valid for. From 2298af4d0b008d844eed12444948339ba7557de7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 6 Feb 2023 15:30:44 -0600 Subject: [PATCH 12/16] Stateless verification of Invoice for Refund Stateless verification of Invoice for Offer Verify that an Invoice was produced from a Refund constructed by the payer using the payer metadata reflected in the Invoice. The payer metadata consists of a 128-bit encrypted nonce and possibly a 256-bit HMAC over the nonce and Refund TLV records (excluding the payer id) using an ExpandedKey. Thus, the HMAC can be reproduced from the refund 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 payer id. --- lightning/src/offers/invoice.rs | 4 +- lightning/src/offers/invoice_request.rs | 4 +- lightning/src/offers/refund.rs | 145 +++++++++++++++++++++++- 3 files changed, 145 insertions(+), 8 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index f5a613aca65..57d37a17b87 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -535,7 +535,9 @@ impl InvoiceContents { InvoiceContents::ForOffer { invoice_request, .. } => { invoice_request.verify(tlv_stream, key, secp_ctx) }, - _ => todo!(), + InvoiceContents::ForRefund { refund, .. } => { + refund.verify(tlv_stream, key, secp_ctx) + }, } } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index a7cdbfc0f15..2294fc45885 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -602,12 +602,12 @@ impl Writeable for InvoiceRequestContents { } /// Valid type range for invoice_request TLV records. -const INVOICE_REQUEST_TYPES: core::ops::Range = 80..160; +pub(super) const INVOICE_REQUEST_TYPES: core::ops::Range = 80..160; /// TLV record type for [`InvoiceRequest::payer_id`] and [`Refund::payer_id`]. /// /// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id -const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88; +pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88; tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, { (80, chain: ChainHash), diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index b87febc7f6d..8f91db597f6 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -85,11 +85,12 @@ use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; -use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::merkle::TlvStream; +use crate::offers::offer::{OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -343,7 +344,7 @@ impl Refund { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) + self.contents.metadata() } /// A chain that the refund is valid for. @@ -455,6 +456,10 @@ impl RefundContents { } } + fn metadata(&self) -> &[u8] { + self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) + } + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.implied_chain()) } @@ -463,6 +468,22 @@ impl RefundContents { ChainHash::using_genesis_block(Network::Bitcoin) } + /// Verifies that the payer metadata was produced from the refund in the TLV stream. + pub(super) fn verify( + &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + let offer_records = tlv_stream.clone().range(OFFER_TYPES); + let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { + match record.r#type { + PAYER_METADATA_TYPE => false, // Should be outside range + INVOICE_REQUEST_PAYER_ID_TYPE => !self.payer.0.derives_keys(), + _ => true, + } + }); + let tlv_stream = offer_records.chain(invreq_records); + signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx) + } + pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes(), @@ -640,7 +661,9 @@ mod tests { use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; use core::convert::TryFrom; use core::time::Duration; + use crate::chain::keysinterface::KeyMaterial; use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; + use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::offer::OfferTlvStreamRef; @@ -726,6 +749,118 @@ mod tests { } } + #[test] + fn builds_refund_with_metadata_derived() { + let desc = "foo".to_string(); + let node_id = payer_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let refund = RefundBuilder + ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000) + .unwrap() + .build().unwrap(); + assert_eq!(refund.payer_id(), node_id); + + // Fails verification with altered fields + let invoice = refund + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(invoice.verify(&expanded_key, &secp_ctx)); + + let mut tlv_stream = refund.as_tlv_stream(); + tlv_stream.2.amount = Some(2000); + + let mut encoded_refund = Vec::new(); + tlv_stream.write(&mut encoded_refund).unwrap(); + + let invoice = Refund::try_from(encoded_refund).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered metadata + let mut tlv_stream = refund.as_tlv_stream(); + let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect(); + tlv_stream.0.metadata = Some(&metadata); + + let mut encoded_refund = Vec::new(); + tlv_stream.write(&mut encoded_refund).unwrap(); + + let invoice = Refund::try_from(encoded_refund).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + } + + #[test] + fn builds_refund_with_derived_payer_id() { + let desc = "foo".to_string(); + let node_id = payer_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let blinded_path = BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] }, + ], + }; + + let refund = RefundBuilder + ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000) + .unwrap() + .path(blinded_path) + .build().unwrap(); + assert_ne!(refund.payer_id(), node_id); + + let invoice = refund + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered fields + let mut tlv_stream = refund.as_tlv_stream(); + tlv_stream.2.amount = Some(2000); + + let mut encoded_refund = Vec::new(); + tlv_stream.write(&mut encoded_refund).unwrap(); + + let invoice = Refund::try_from(encoded_refund).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + + // Fails verification with altered payer_id + let mut tlv_stream = refund.as_tlv_stream(); + let payer_id = pubkey(1); + tlv_stream.2.payer_id = Some(&payer_id); + + let mut encoded_refund = Vec::new(); + tlv_stream.write(&mut encoded_refund).unwrap(); + + let invoice = Refund::try_from(encoded_refund).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + assert!(!invoice.verify(&expanded_key, &secp_ctx)); + } + #[test] fn builds_refund_with_absolute_expiry() { let future_expiry = Duration::from_secs(u64::max_value()); From 259aa9aac37097c462a25cc39e83ebe2b86ac4bb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 4 Apr 2023 14:59:09 -0500 Subject: [PATCH 13/16] DRY up verification of invreq TLV records --- lightning/src/offers/invoice.rs | 34 ++++++++++++++++++++----- lightning/src/offers/invoice_request.rs | 28 ++++++++------------ lightning/src/offers/refund.rs | 33 +++++++++--------------- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 57d37a17b87..17908391a5b 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -108,12 +108,13 @@ use crate::ln::PaymentHash; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; -use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self}; -use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::refund::{Refund, RefundContents}; +use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents}; +use crate::offers::signer; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer}; @@ -531,13 +532,32 @@ impl InvoiceContents { fn verify( &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> bool { - match self { + let offer_records = tlv_stream.clone().range(OFFER_TYPES); + let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { + match record.r#type { + PAYER_METADATA_TYPE => false, // Should be outside range + INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(), + _ => true, + } + }); + let tlv_stream = offer_records.chain(invreq_records); + + let (metadata, payer_id, iv_bytes) = match self { InvoiceContents::ForOffer { invoice_request, .. } => { - invoice_request.verify(tlv_stream, key, secp_ctx) + (invoice_request.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES) }, InvoiceContents::ForRefund { refund, .. } => { - refund.verify(tlv_stream, key, secp_ctx) + (refund.metadata(), refund.payer_id(), REFUND_IV_BYTES) }, + }; + + signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) + } + + fn derives_keys(&self) -> bool { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(), + InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(), } } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 2294fc45885..6b5c7786220 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -66,10 +66,10 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self}; -use crate::offers::offer::{OFFER_TYPES, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial, self}; +use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -78,7 +78,7 @@ use crate::prelude::*; const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); -const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; +pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; /// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow. /// @@ -528,24 +528,16 @@ impl InvoiceRequestContents { self.inner.metadata() } + pub(super) fn derives_keys(&self) -> bool { + self.inner.payer.0.derives_keys() + } + pub(super) fn chain(&self) -> ChainHash { self.inner.chain() } - /// Verifies that the payer metadata was produced from the invoice request in the TLV stream. - pub(super) fn verify( - &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { - let offer_records = tlv_stream.clone().range(OFFER_TYPES); - let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { - match record.r#type { - PAYER_METADATA_TYPE => false, // Should be outside range - INVOICE_REQUEST_PAYER_ID_TYPE => !self.inner.payer.0.derives_keys(), - _ => true, - } - }); - let tlv_stream = offer_records.chain(invreq_records); - signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx) + pub(super) fn payer_id(&self) -> PublicKey { + self.payer_id } pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 8f91db597f6..582c5b7eb18 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -85,12 +85,11 @@ use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; -use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::merkle::TlvStream; -use crate::offers::offer::{OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PAYER_METADATA_TYPE, PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial, self}; +use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::offers::signer::{Metadata, MetadataMaterial}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -100,7 +99,7 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; -const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~"; +pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~"; /// Builds a [`Refund`] for the "offer for money" flow. /// @@ -456,7 +455,7 @@ impl RefundContents { } } - fn metadata(&self) -> &[u8] { + pub(super) fn metadata(&self) -> &[u8] { self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) } @@ -468,20 +467,12 @@ impl RefundContents { ChainHash::using_genesis_block(Network::Bitcoin) } - /// Verifies that the payer metadata was produced from the refund in the TLV stream. - pub(super) fn verify( - &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { - let offer_records = tlv_stream.clone().range(OFFER_TYPES); - let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { - match record.r#type { - PAYER_METADATA_TYPE => false, // Should be outside range - INVOICE_REQUEST_PAYER_ID_TYPE => !self.payer.0.derives_keys(), - _ => true, - } - }); - let tlv_stream = offer_records.chain(invreq_records); - signer::verify_metadata(self.metadata(), key, IV_BYTES, self.payer_id, tlv_stream, secp_ctx) + pub(super) fn derives_keys(&self) -> bool { + self.payer.0.derives_keys() + } + + pub(super) fn payer_id(&self) -> PublicKey { + self.payer_id } pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { From e1a6bc3cadb8c89ba38afb5846fb80f349d99449 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 5 Apr 2023 00:04:41 -0500 Subject: [PATCH 14/16] Extract keys from Offer::metadata to sign Invoice For offers where the signing pubkey is derived, the keys need to be extracted from the Offer::metadata in order to sign an invoice. Parameterize InvoiceBuilder such that a build_and_sign method is available for this situation. --- lightning/src/offers/invoice.rs | 185 +++++++++++++++++++++--- lightning/src/offers/invoice_request.rs | 63 +++++++- lightning/src/offers/offer.rs | 24 +-- lightning/src/offers/parse.rs | 2 + lightning/src/offers/refund.rs | 6 +- lightning/src/offers/signer.rs | 42 ++++-- 6 files changed, 267 insertions(+), 55 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 17908391a5b..1635d956cbe 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -97,11 +97,11 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; -use core::convert::TryFrom; +use core::convert::{Infallible, TryFrom}; use core::time::Duration; use crate::io; use crate::ln::PaymentHash; @@ -136,28 +136,31 @@ pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", " /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Refund`]: crate::offers::refund::Refund /// [module-level documentation]: self -pub struct InvoiceBuilder<'a> { +pub struct InvoiceBuilder<'a, S: SigningPubkeyStrategy> { invreq_bytes: &'a Vec, invoice: InvoiceContents, + keys: Option, + signing_pubkey_strategy: core::marker::PhantomData, } -impl<'a> InvoiceBuilder<'a> { +/// Indicates how [`Invoice::signing_pubkey`] was set. +pub trait SigningPubkeyStrategy {} + +/// [`Invoice::signing_pubkey`] was explicitly set. +pub struct ExplicitSigningPubkey {} + +/// [`Invoice::signing_pubkey`] was derived. +pub struct DerivedSigningPubkey {} + +impl SigningPubkeyStrategy for ExplicitSigningPubkey {} +impl SigningPubkeyStrategy for DerivedSigningPubkey {} + +impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { pub(super) fn for_offer( invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, payment_hash: PaymentHash ) -> Result { - let amount_msats = match invoice_request.amount_msats() { - Some(amount_msats) => amount_msats, - None => match invoice_request.contents.inner.offer.amount() { - Some(Amount::Bitcoin { amount_msats }) => { - amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) - .ok_or(SemanticError::InvalidAmount)? - }, - Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), - None => return Err(SemanticError::MissingAmount), - }, - }; - + let amount_msats = Self::check_amount_msats(invoice_request)?; let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: InvoiceFields { @@ -167,7 +170,7 @@ impl<'a> InvoiceBuilder<'a> { }, }; - Self::new(&invoice_request.bytes, contents) + Self::new(&invoice_request.bytes, contents, None) } pub(super) fn for_refund( @@ -183,15 +186,57 @@ impl<'a> InvoiceBuilder<'a> { }, }; - Self::new(&refund.bytes, contents) + Self::new(&refund.bytes, contents, None) } +} - fn new(invreq_bytes: &'a Vec, contents: InvoiceContents) -> Result { +impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { + pub(super) fn for_offer_using_keys( + invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, + created_at: Duration, payment_hash: PaymentHash, keys: KeyPair + ) -> Result { + let amount_msats = Self::check_amount_msats(invoice_request)?; + let contents = InvoiceContents::ForOffer { + invoice_request: invoice_request.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, + fallbacks: None, features: Bolt12InvoiceFeatures::empty(), + signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(), + }, + }; + + Self::new(&invoice_request.bytes, contents, Some(keys)) + } +} + +impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { + fn check_amount_msats(invoice_request: &InvoiceRequest) -> Result { + match invoice_request.amount_msats() { + Some(amount_msats) => Ok(amount_msats), + None => match invoice_request.contents.inner.offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => { + amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount) + }, + Some(Amount::Currency { .. }) => Err(SemanticError::UnsupportedCurrency), + None => Err(SemanticError::MissingAmount), + }, + } + } + + fn new( + invreq_bytes: &'a Vec, contents: InvoiceContents, keys: Option + ) -> Result { if contents.fields().payment_paths.is_empty() { return Err(SemanticError::MissingPaths); } - Ok(Self { invreq_bytes, invoice: contents }) + Ok(Self { + invreq_bytes, + invoice: contents, + keys, + signing_pubkey_strategy: core::marker::PhantomData, + }) } /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry @@ -248,7 +293,9 @@ impl<'a> InvoiceBuilder<'a> { self.invoice.fields_mut().features.set_basic_mpp_optional(); self } +} +impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { /// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by /// [`UnsignedInvoice::sign`]. pub fn build(self) -> Result, SemanticError> { @@ -258,11 +305,36 @@ impl<'a> InvoiceBuilder<'a> { } } - let InvoiceBuilder { invreq_bytes, invoice } = self; + let InvoiceBuilder { invreq_bytes, invoice, .. } = self; Ok(UnsignedInvoice { invreq_bytes, invoice }) } } +impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { + /// Builds a signed [`Invoice`] after checking for valid semantics. + pub fn build_and_sign( + self, secp_ctx: &Secp256k1 + ) -> Result { + #[cfg(feature = "std")] { + if self.invoice.is_offer_or_refund_expired() { + return Err(SemanticError::AlreadyExpired); + } + } + + let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; + let keys = match &invoice { + InvoiceContents::ForOffer { .. } => keys.unwrap(), + InvoiceContents::ForRefund { .. } => unreachable!(), + }; + + let unsigned_invoice = UnsignedInvoice { invreq_bytes, invoice }; + let invoice = unsigned_invoice + .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .unwrap(); + Ok(invoice) + } +} + /// A semantically valid [`Invoice`] that hasn't been signed. pub struct UnsignedInvoice<'a> { invreq_bytes: &'a Vec, @@ -551,7 +623,10 @@ impl InvoiceContents { }, }; - signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) + match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) { + Ok(_) => true, + Err(()) => false, + } } fn derives_keys(&self) -> bool { @@ -831,8 +906,10 @@ mod tests { use bitcoin::util::schnorr::TweakedPublicKey; use core::convert::TryFrom; use core::time::Duration; - use crate::ln::msgs::DecodeError; + use crate::chain::keysinterface::KeyMaterial; use crate::ln::features::Bolt12InvoiceFeatures; + use crate::ln::inbound_payment::ExpandedKey; + use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; @@ -840,6 +917,7 @@ mod tests { use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; use crate::offers::test_utils::*; + use crate::onion_message::{BlindedHop, BlindedPath}; use crate::util::ser::{BigSize, Iterable, Writeable}; trait ToBytes { @@ -1084,6 +1162,67 @@ mod tests { } } + #[test] + fn builds_invoice_from_offer_using_derived_keys() { + let desc = "foo".to_string(); + let node_id = recipient_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let blinded_path = BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] }, + ], + }; + + let offer = OfferBuilder + ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + .amount_msats(1000) + .path(blinded_path) + .build().unwrap(); + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + if let Err(e) = invoice_request + .verify_and_respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx + ) + .unwrap() + .build_and_sign(&secp_ctx) + { + panic!("error building invoice: {:?}", e); + } + + let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32])); + match invoice_request.verify_and_respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx + ) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidMetadata), + } + + let desc = "foo".to_string(); + let offer = OfferBuilder + ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + .amount_msats(1000) + .build().unwrap(); + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + match invoice_request.verify_and_respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx + ) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidMetadata), + } + } + #[test] fn builds_invoice_with_relative_expiry() { let now = now(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 6b5c7786220..92fabd6fdf0 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -64,8 +64,8 @@ use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; -use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self}; +use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self}; use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -469,7 +469,7 @@ impl InvoiceRequest { #[cfg(feature = "std")] pub fn respond_with( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash - ) -> Result { + ) -> Result, SemanticError> { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); @@ -497,7 +497,7 @@ impl InvoiceRequest { pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, created_at: core::time::Duration - ) -> Result { + ) -> Result, SemanticError> { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } @@ -505,11 +505,60 @@ impl InvoiceRequest { InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } - /// Verifies that the request was for an offer created using the given key. + /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses + /// derived signing keys from the originating [`Offer`] to sign the [`Invoice`]. Must use the + /// same [`ExpandedKey`] as the one used to create the offer. + /// + /// See [`InvoiceRequest::respond_with`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + #[cfg(feature = "std")] + pub fn verify_and_respond_using_derived_keys( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + expanded_key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result, SemanticError> { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.verify_and_respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at, expanded_key, secp_ctx + ) + } + + /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses + /// derived signing keys from the originating [`Offer`] to sign the [`Invoice`]. Must use the + /// same [`ExpandedKey`] as the one used to create the offer. + /// + /// See [`InvoiceRequest::respond_with_no_std`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + pub fn verify_and_respond_using_derived_keys_no_std( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + created_at: core::time::Duration, expanded_key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result, SemanticError> { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + let keys = match self.verify(expanded_key, secp_ctx) { + Err(()) => return Err(SemanticError::InvalidMetadata), + Ok(None) => return Err(SemanticError::InvalidMetadata), + Ok(Some(keys)) => keys, + }; + + InvoiceBuilder::for_offer_using_keys(self, payment_paths, created_at, payment_hash, keys) + } + + /// Verifies that the request was for an offer created using the given key. Returns the derived + /// keys need to sign an [`Invoice`] for the request if they could be extracted from the + /// metadata. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice pub fn verify( &self, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { - self.contents.inner.offer.verify(TlvStream::new(&self.bytes), key, secp_ctx) + ) -> Result, ()> { + self.contents.inner.offer.verify(&self.bytes, key, secp_ctx) } #[cfg(test)] diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 9f22e9af184..d2918e80942 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -68,7 +68,7 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use core::convert::TryFrom; use core::num::NonZeroU64; use core::ops::Deref; @@ -92,7 +92,7 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; -const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; +pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; /// Builds an [`Offer`] for the "offer to be paid" flow. /// @@ -615,11 +615,11 @@ impl OfferContents { /// Verifies that the offer metadata was produced from the offer in the TLV stream. pub(super) fn verify( - &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> bool { + &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result, ()> { match self.metadata() { Some(metadata) => { - let tlv_stream = tlv_stream.range(OFFER_TYPES).filter(|record| { + let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| { match record.r#type { OFFER_METADATA_TYPE => false, OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(), @@ -630,7 +630,7 @@ impl OfferContents { metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx ) }, - None => false, + None => Err(()), } } @@ -962,7 +962,7 @@ mod tests { let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); @@ -975,7 +975,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let mut tlv_stream = offer.as_tlv_stream(); @@ -989,7 +989,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -1019,7 +1019,7 @@ mod tests { let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); @@ -1032,7 +1032,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered signing pubkey let mut tlv_stream = offer.as_tlv_stream(); @@ -1046,7 +1046,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(!invoice_request.verify(&expanded_key, &secp_ctx)); + assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 6afd4d68fef..3f1a9c887ec 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -171,6 +171,8 @@ pub enum SemanticError { InvalidQuantity, /// A quantity or quantity bounds was provided but was not expected. UnexpectedQuantity, + /// Metadata could not be used to verify the offers message. + InvalidMetadata, /// Metadata was provided but was not expected. UnexpectedMetadata, /// Payer metadata was expected but was missing. diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 582c5b7eb18..899586ba29d 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -84,7 +84,7 @@ use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; -use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; +use crate::offers::invoice::{BlindedPayInfo, ExplicitSigningPubkey, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; @@ -392,7 +392,7 @@ impl Refund { pub fn respond_with( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, signing_pubkey: PublicKey, - ) -> Result { + ) -> Result, SemanticError> { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); @@ -423,7 +423,7 @@ impl Refund { pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, signing_pubkey: PublicKey, created_at: Duration - ) -> Result { + ) -> Result, SemanticError> { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index f6141e59699..7229775aa0b 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -172,14 +172,40 @@ pub(super) fn verify_metadata<'a, T: secp256k1::Signing>( metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, secp_ctx: &Secp256k1 -) -> bool { +) -> Result, ()> { + let hmac = hmac_for_message(metadata, expanded_key, iv_bytes, tlv_stream)?; + + if metadata.len() == Nonce::LENGTH { + let derived_keys = KeyPair::from_secret_key( + secp_ctx, &SecretKey::from_slice(hmac.as_inner()).unwrap() + ); + if fixed_time_eq(&signing_pubkey.serialize(), &derived_keys.public_key().serialize()) { + Ok(Some(derived_keys)) + } else { + Err(()) + } + } else if metadata[Nonce::LENGTH..].len() == Sha256::LEN { + if fixed_time_eq(&metadata[Nonce::LENGTH..], &hmac.into_inner()) { + Ok(None) + } else { + Err(()) + } + } else { + Err(()) + } +} + +fn hmac_for_message<'a>( + metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + tlv_stream: impl core::iter::Iterator> +) -> Result, ()> { if metadata.len() < Nonce::LENGTH { - return false; + return Err(()); } let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) { Ok(nonce) => nonce, - Err(_) => return false, + Err(_) => return Err(()), }; let mut hmac = expanded_key.hmac_for_offer(nonce, iv_bytes); @@ -189,13 +215,9 @@ pub(super) fn verify_metadata<'a, T: secp256k1::Signing>( if metadata.len() == Nonce::LENGTH { hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); - let hmac = Hmac::from_engine(hmac); - let derived_pubkey = SecretKey::from_slice(hmac.as_inner()).unwrap().public_key(&secp_ctx); - fixed_time_eq(&signing_pubkey.serialize(), &derived_pubkey.serialize()) - } else if metadata[Nonce::LENGTH..].len() == Sha256::LEN { - hmac.input(DERIVED_METADATA_HMAC_INPUT); - fixed_time_eq(&metadata[Nonce::LENGTH..], &Hmac::from_engine(hmac).into_inner()) } else { - false + hmac.input(DERIVED_METADATA_HMAC_INPUT); } + + Ok(Hmac::from_engine(hmac)) } From c8a847ae11f50765c59c79503c127235131ee479 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 10 Apr 2023 11:58:14 -0500 Subject: [PATCH 15/16] Support responding to refunds with transient keys --- lightning/src/offers/invoice.rs | 43 +++++++++++++++++++++++++---- lightning/src/offers/refund.rs | 49 +++++++++++++++++++++++++++++++-- lightning/src/offers/signer.rs | 8 ++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 1635d956cbe..0cc6c407c1d 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -207,6 +207,22 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { Self::new(&invoice_request.bytes, contents, Some(keys)) } + + pub(super) fn for_refund_using_keys( + refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, keys: KeyPair, + ) -> Result { + let contents = InvoiceContents::ForRefund { + refund: refund.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, + amount_msats: refund.amount_msats(), fallbacks: None, + features: Bolt12InvoiceFeatures::empty(), signing_pubkey: keys.public_key(), + }, + }; + + Self::new(&refund.bytes, contents, Some(keys)) + } } impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { @@ -322,12 +338,9 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; - let keys = match &invoice { - InvoiceContents::ForOffer { .. } => keys.unwrap(), - InvoiceContents::ForRefund { .. } => unreachable!(), - }; - let unsigned_invoice = UnsignedInvoice { invreq_bytes, invoice }; + + let keys = keys.unwrap(); let invoice = unsigned_invoice .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) .unwrap(); @@ -1223,6 +1236,26 @@ mod tests { } } + #[test] + fn builds_invoice_from_refund_using_derived_keys() { + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + if let Err(e) = refund + .respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &entropy + ) + .unwrap() + .build_and_sign(&secp_ctx) + { + panic!("error building invoice: {:?}", e); + } + } + #[test] fn builds_invoice_with_relative_expiry() { let now = now(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 899586ba29d..f677a2a9cdb 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -84,12 +84,12 @@ use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; -use crate::offers::invoice::{BlindedPayInfo, ExplicitSigningPubkey, InvoiceBuilder}; +use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -431,6 +431,51 @@ impl Refund { InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) } + /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses + /// derived signing keys to sign the [`Invoice`]. + /// + /// See [`Refund::respond_with`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + #[cfg(feature = "std")] + pub fn respond_using_derived_keys( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + expanded_key: &ExpandedKey, entropy_source: ES + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at, expanded_key, entropy_source + ) + } + + /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses + /// derived signing keys to sign the [`Invoice`]. + /// + /// See [`Refund::respond_with_no_std`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + pub fn respond_using_derived_keys_no_std( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + created_at: core::time::Duration, expanded_key: &ExpandedKey, entropy_source: ES + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + let nonce = Nonce::from_entropy_source(entropy_source); + let keys = signer::derive_keys(nonce, expanded_key); + InvoiceBuilder::for_refund_using_keys(self, payment_paths, created_at, payment_hash, keys) + } + #[cfg(test)] fn as_tlv_stream(&self) -> RefundTlvStreamRef { self.contents.as_tlv_stream() diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 7229775aa0b..8d5f98e6f6b 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -162,6 +162,14 @@ impl MetadataMaterial { } } +pub(super) fn derive_keys(nonce: Nonce, expanded_key: &ExpandedKey) -> KeyPair { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Invoice ~~~~"; + let secp_ctx = Secp256k1::new(); + let hmac = Hmac::from_engine(expanded_key.hmac_for_offer(nonce, IV_BYTES)); + let privkey = SecretKey::from_slice(hmac.as_inner()).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &privkey) +} + /// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: /// - a 128-bit [`Nonce`] and possibly /// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`]. From 8afe6940200769b9df9e9ecfda2a8390919a6cf2 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 17 Apr 2023 18:31:52 -0500 Subject: [PATCH 16/16] DRY up InvoiceFields construction --- lightning/src/offers/invoice.rs | 47 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 0cc6c407c1d..b2717f7338f 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -161,13 +161,12 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { created_at: Duration, payment_hash: PaymentHash ) -> Result { let amount_msats = Self::check_amount_msats(invoice_request)?; + let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), - fields: InvoiceFields { - payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, - fallbacks: None, features: Bolt12InvoiceFeatures::empty(), - signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(), - }, + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), }; Self::new(&invoice_request.bytes, contents, None) @@ -177,13 +176,12 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey ) -> Result { + let amount_msats = refund.amount_msats(); let contents = InvoiceContents::ForRefund { refund: refund.contents.clone(), - fields: InvoiceFields { - payment_paths, created_at, relative_expiry: None, payment_hash, - amount_msats: refund.amount_msats(), fallbacks: None, - features: Bolt12InvoiceFeatures::empty(), signing_pubkey, - }, + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), }; Self::new(&refund.bytes, contents, None) @@ -196,13 +194,12 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { created_at: Duration, payment_hash: PaymentHash, keys: KeyPair ) -> Result { let amount_msats = Self::check_amount_msats(invoice_request)?; + let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), - fields: InvoiceFields { - payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, - fallbacks: None, features: Bolt12InvoiceFeatures::empty(), - signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(), - }, + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), }; Self::new(&invoice_request.bytes, contents, Some(keys)) @@ -212,13 +209,13 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, payment_hash: PaymentHash, keys: KeyPair, ) -> Result { + let amount_msats = refund.amount_msats(); + let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForRefund { refund: refund.contents.clone(), - fields: InvoiceFields { - payment_paths, created_at, relative_expiry: None, payment_hash, - amount_msats: refund.amount_msats(), fallbacks: None, - features: Bolt12InvoiceFeatures::empty(), signing_pubkey: keys.public_key(), - }, + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), }; Self::new(&refund.bytes, contents, Some(keys)) @@ -240,6 +237,16 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { } } + fn fields( + payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, amount_msats: u64, signing_pubkey: PublicKey + ) -> InvoiceFields { + InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, + fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + } + } + fn new( invreq_bytes: &'a Vec, contents: InvoiceContents, keys: Option ) -> Result {