diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 0c6d6f2b804..2d15876bf95 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -19,14 +19,14 @@ 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; -use core::convert::TryInto; +use core::convert::{TryFrom, 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; @@ -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,14 +57,76 @@ 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, } } + + /// 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, Debug, PartialEq)] +pub(crate) struct Nonce(pub(crate) [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 + } +} + +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 { diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 48b8cec3536..b2717f7338f 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -97,22 +97,24 @@ 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::{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; 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::offer::{Amount, OfferTlvStream, OfferTlvStreamRef}; +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, 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}; @@ -123,7 +125,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 @@ -134,62 +136,130 @@ const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature") /// [`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.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 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.offer.signing_pubkey(), - }, + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), }; - Self::new(&invoice_request.bytes, contents) + Self::new(&invoice_request.bytes, contents, None) } pub(super) fn for_refund( 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) + 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 signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); + let contents = InvoiceContents::ForOffer { + invoice_request: invoice_request.contents.clone(), + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), + }; + + 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 amount_msats = refund.amount_msats(); + let signing_pubkey = keys.public_key(); + let contents = InvoiceContents::ForRefund { + refund: refund.contents.clone(), + fields: Self::fields( + payment_paths, created_at, payment_hash, amount_msats, signing_pubkey + ), + }; + + Self::new(&refund.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 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 { 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 @@ -246,7 +316,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> { @@ -256,11 +328,33 @@ 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 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(); + Ok(invoice) + } +} + /// A semantically valid [`Invoice`] that hasn't been signed. pub struct UnsignedInvoice<'a> { invreq_bytes: &'a Vec, @@ -313,7 +407,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 +419,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`]. /// @@ -474,8 +570,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 { @@ -491,7 +594,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(), } } @@ -517,6 +621,41 @@ impl InvoiceContents { } } + 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.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.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES) + }, + InvoiceContents::ForRefund { refund, .. } => { + (refund.metadata(), refund.payer_id(), REFUND_IV_BYTES) + }, + }; + + match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) { + Ok(_) => true, + Err(()) => false, + } + } + + fn derives_keys(&self) -> bool { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(), + InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(), + } + } + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { let (payer, offer, invoice_request) = match self { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(), @@ -777,68 +916,30 @@ 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::chain::keysinterface::KeyMaterial; + use crate::ln::features::Bolt12InvoiceFeatures; + use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::ln::features::{BlindedHopFeatures, 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::offers::test_utils::*; use crate::onion_message::{BlindedHop, BlindedPath}; 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 +956,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(); @@ -1133,6 +1182,87 @@ 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_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/invoice_request.rs b/lightning/src/offers/invoice_request.rs index a1a0520c622..92fabd6fdf0 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -54,18 +54,22 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{Message, PublicKey}; +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, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; -use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; +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}; +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; @@ -74,25 +78,83 @@ use crate::prelude::*; const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); +pub(super) 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 { - payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None, - amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, - payer_id, payer_note: None, - }, + 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, } } @@ -143,9 +205,10 @@ impl<'a> InvoiceRequestBuilder<'a> { 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); @@ -170,13 +233,79 @@ impl<'a> InvoiceRequestBuilder<'a> { 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.chain = Some(chain); @@ -199,8 +328,7 @@ impl<'a> InvoiceRequestBuilder<'a> { } pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { - let InvoiceRequestBuilder { offer, invoice_request } = self; - UnsignedInvoiceRequest { offer, invoice_request } + self.build_without_checks().0 } } @@ -250,7 +378,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,15 +389,22 @@ 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 { + 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, } @@ -278,7 +414,7 @@ impl InvoiceRequest { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0[..] + self.contents.metadata() } /// A chain from [`Offer::chains`] that the offer is valid for. @@ -291,17 +427,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. @@ -312,7 +448,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`]. @@ -322,18 +459,17 @@ 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( &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"); @@ -341,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 @@ -357,12 +493,11 @@ 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, created_at: core::time::Duration - ) -> Result { + ) -> Result, SemanticError> { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } @@ -370,6 +505,62 @@ impl InvoiceRequest { InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } + /// 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 + ) -> Result, ()> { + self.contents.inner.offer.verify(&self.bytes, key, secp_ctx) + } + #[cfg(test)] fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) = @@ -382,13 +573,41 @@ impl InvoiceRequest { } impl InvoiceRequestContents { + pub fn metadata(&self) -> &[u8] { + 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() + } + + pub(super) fn payer_id(&self) -> PublicKey { + self.payer_id + } + + 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 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(); @@ -403,7 +622,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(), }; @@ -423,12 +642,20 @@ impl Writeable for InvoiceRequestContents { } } -tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { +/// Valid type range for invoice_request TLV records. +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 +pub(super) 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)), }); @@ -498,7 +725,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)?; @@ -521,7 +748,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, }) } } @@ -532,47 +762,24 @@ 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")] 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}; 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()) @@ -661,6 +868,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); @@ -1008,12 +1357,28 @@ 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()) .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/merkle.rs b/lightning/src/offers/merkle.rs index 94a1eac0ca4..3b05899a8f5 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -143,28 +143,38 @@ 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> { +#[derive(Clone)] +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 +222,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 +312,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 diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 2da6fac08ff..0fb20f42d79 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -19,3 +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 405e2e278d8..d2918e80942 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -68,16 +68,21 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{KeyPair, 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::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}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -87,30 +92,90 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +pub(super) 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 by + /// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an + /// [`ExpandedKey`]. + /// + /// [`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, + 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 +192,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 +261,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 +321,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 +334,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 +373,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. @@ -358,8 +439,53 @@ impl Offer { self.contents.signing_pubkey() } - /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which - /// will be reflected in the `Invoice` response. + /// 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 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. + /// + /// [`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 [`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 @@ -373,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); } @@ -406,6 +532,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 { @@ -483,6 +613,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, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result, ()> { + match self.metadata() { + Some(metadata) => { + 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(), + _ => true, + } + }); + signer::verify_metadata( + metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx + ) + }, + None => Err(()), + } + } + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), @@ -498,7 +649,7 @@ impl OfferContents { OfferTlvStreamRef { chains: self.chains.as_ref(), - metadata: self.metadata.as_ref(), + metadata: self.metadata(), currency, amount, description: Some(&self.description), @@ -570,9 +721,18 @@ impl Quantity { } } -tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, { +/// Valid type range for offer TLV records. +pub(super) 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)), @@ -581,7 +741,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 { @@ -616,6 +776,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 => { @@ -666,26 +828,20 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + 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::*; 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(); @@ -774,21 +930,125 @@ 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])); 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).is_ok()); + + // 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).is_err()); + + // 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).is_err()); + } + + #[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).is_ok()); + + // 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).is_err()); + + // 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).is_err()); + } + #[test] fn builds_offer_with_amount() { let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 }; 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/payer.rs b/lightning/src/offers/payer.rs index 7e1da769eda..bfc02b5dbcb 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::*; @@ -17,9 +18,16 @@ use crate::prelude::*; /// [`InvoiceRequest::payer_id`]. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id -#[derive(Clone, Debug, PartialEq)] -pub(super) struct PayerContents(pub Vec); +#[derive(Clone, Debug)] +#[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/refund.rs b/lightning/src/offers/refund.rs index cc0388c0241..f677a2a9cdb 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -73,19 +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::{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, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -95,16 +99,19 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +pub(super) 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. /// @@ -117,13 +124,48 @@ impl RefundBuilder { return Err(SemanticError::InvalidAmount); } - 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, - }; + let metadata = Metadata::Bytes(metadata); + 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 @@ -190,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 @@ -216,7 +278,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 +288,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 @@ -279,7 +343,7 @@ impl Refund { /// /// [`payer_id`]: Self::payer_id pub fn metadata(&self) -> &[u8] { - &self.contents.payer.0 + self.contents.metadata() } /// A chain that the refund is valid for. @@ -317,19 +381,18 @@ 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( &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"); @@ -337,7 +400,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 @@ -356,12 +419,11 @@ 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, signing_pubkey: PublicKey, created_at: Duration - ) -> Result { + ) -> Result, SemanticError> { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } @@ -369,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() @@ -393,6 +500,10 @@ impl RefundContents { } } + 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.implied_chain()) } @@ -401,9 +512,17 @@ impl RefundContents { ChainHash::using_genesis_block(Network::Bitcoin) } + 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 { let payer = PayerTlvStreamRef { - metadata: Some(&self.payer.0), + metadata: self.payer.0.as_bytes(), }; let offer = OfferTlvStreamRef { @@ -507,7 +626,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() { @@ -575,33 +694,22 @@ 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::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; 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; } @@ -677,6 +785,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()); @@ -822,6 +1042,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() diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs new file mode 100644 index 00000000000..8d5f98e6f6b --- /dev/null +++ b/lightning/src/offers/signer.rs @@ -0,0 +1,231 @@ +// 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::cmp::fixed_time_eq; +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +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::*; + +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 { + // 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, + } + } + + 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 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 { + 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) + } +} + +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`]. +/// +/// 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: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator>, + secp_ctx: &Secp256k1 +) -> 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 Err(()); + } + + let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) { + Ok(nonce) => nonce, + Err(_) => return Err(()), + }; + 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); + } else { + hmac.input(DERIVED_METADATA_HMAC_INPUT); + } + + Ok(Hmac::from_engine(hmac)) +} diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs new file mode 100644 index 00000000000..43664079dbd --- /dev/null +++ b/lightning/src/offers/test_utils.rs @@ -0,0 +1,119 @@ +// 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::chain::keysinterface::EntropySource; +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") +} + +pub(super) struct FixedEntropy; + +impl EntropySource for FixedEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [42; 32] + } +} 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]