Support responding to refunds with transient keys
authorJeffrey Czyz <jkczyz@gmail.com>
Mon, 10 Apr 2023 16:58:14 +0000 (11:58 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Apr 2023 02:31:07 +0000 (21:31 -0500)
lightning/src/offers/invoice.rs
lightning/src/offers/refund.rs
lightning/src/offers/signer.rs

index 1635d956cbe88d0b55f5726836eae926cac942bf..0cc6c407c1dc0e39a3082adc11251e8881ac11f8 100644 (file)
@@ -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<Self, SemanticError> {
+               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();
index 899586ba29d6b3875c225e91d40956c611a98793..f677a2a9cdb61fcdf5f5fe9f5389a6dba895512c 100644 (file)
@@ -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<ES: Deref>(
+               &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+               expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, 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<ES: Deref>(
+               &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+               created_at: core::time::Duration, expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, 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()
index 7229775aa0b3b959bd83df3e70269f172a5bc153..8d5f98e6f6b050993474bbedbcc9a0f25c409980 100644 (file)
@@ -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`].