From c8a847ae11f50765c59c79503c127235131ee479 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 10 Apr 2023 11:58:14 -0500 Subject: [PATCH] Support responding to refunds with transient keys --- lightning/src/offers/invoice.rs | 43 +++++++++++++++++++++++++---- lightning/src/offers/refund.rs | 49 +++++++++++++++++++++++++++++++-- lightning/src/offers/signer.rs | 8 ++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 1635d956c..0cc6c407c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -207,6 +207,22 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { Self::new(&invoice_request.bytes, contents, Some(keys)) } + + pub(super) fn for_refund_using_keys( + refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, keys: KeyPair, + ) -> Result { + let contents = InvoiceContents::ForRefund { + refund: refund.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, + amount_msats: refund.amount_msats(), fallbacks: None, + features: Bolt12InvoiceFeatures::empty(), signing_pubkey: keys.public_key(), + }, + }; + + Self::new(&refund.bytes, contents, Some(keys)) + } } impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { @@ -322,12 +338,9 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; - let keys = match &invoice { - InvoiceContents::ForOffer { .. } => keys.unwrap(), - InvoiceContents::ForRefund { .. } => unreachable!(), - }; - let unsigned_invoice = UnsignedInvoice { invreq_bytes, invoice }; + + let keys = keys.unwrap(); let invoice = unsigned_invoice .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) .unwrap(); @@ -1223,6 +1236,26 @@ mod tests { } } + #[test] + fn builds_invoice_from_refund_using_derived_keys() { + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + if let Err(e) = refund + .respond_using_derived_keys_no_std( + payment_paths(), payment_hash(), now(), &expanded_key, &entropy + ) + .unwrap() + .build_and_sign(&secp_ctx) + { + panic!("error building invoice: {:?}", e); + } + } + #[test] fn builds_invoice_with_relative_expiry() { let now = now(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 899586ba2..f677a2a9c 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -84,12 +84,12 @@ use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; -use crate::offers::invoice::{BlindedPayInfo, ExplicitSigningPubkey, InvoiceBuilder}; +use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::onion_message::BlindedPath; use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -431,6 +431,51 @@ impl Refund { InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) } + /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses + /// derived signing keys to sign the [`Invoice`]. + /// + /// See [`Refund::respond_with`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + #[cfg(feature = "std")] + pub fn respond_using_derived_keys( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + expanded_key: &ExpandedKey, entropy_source: ES + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at, expanded_key, entropy_source + ) + } + + /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses + /// derived signing keys to sign the [`Invoice`]. + /// + /// See [`Refund::respond_with_no_std`] for further details. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + pub fn respond_using_derived_keys_no_std( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + created_at: core::time::Duration, expanded_key: &ExpandedKey, entropy_source: ES + ) -> Result, SemanticError> + where + ES::Target: EntropySource, + { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + let nonce = Nonce::from_entropy_source(entropy_source); + let keys = signer::derive_keys(nonce, expanded_key); + InvoiceBuilder::for_refund_using_keys(self, payment_paths, created_at, payment_hash, keys) + } + #[cfg(test)] fn as_tlv_stream(&self) -> RefundTlvStreamRef { self.contents.as_tlv_stream() diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 7229775aa..8d5f98e6f 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -162,6 +162,14 @@ impl MetadataMaterial { } } +pub(super) fn derive_keys(nonce: Nonce, expanded_key: &ExpandedKey) -> KeyPair { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Invoice ~~~~"; + let secp_ctx = Secp256k1::new(); + let hmac = Hmac::from_engine(expanded_key.hmac_for_offer(nonce, IV_BYTES)); + let privkey = SecretKey::from_slice(hmac.as_inner()).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &privkey) +} + /// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: /// - a 128-bit [`Nonce`] and possibly /// - a [`Sha256`] hash of the nonce and the TLV records using the [`ExpandedKey`]. -- 2.39.5