Bolt12Invoice for Offer without signing_pubkey
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 23 Apr 2024 23:35:05 +0000 (18:35 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 26 Apr 2024 23:14:09 +0000 (18:14 -0500)
When parsing a Bolt12Invoice use both the Offer's signing_pubkey and
paths to determine if it is for an Offer or a Refund. Previously, an
Offer was required to have a signing_pubkey. But now that it is
optional, the Offers paths can be used to make the determination.
Additionally, check that the invoice matches one of the blinded node ids
from the paths' last hops.

lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs

index 7dd57365321078ef9172d4ba5efaa9add5a4381c..75bbcab93f8e21f493c71c173e05dbe210c30149 100644 (file)
@@ -1434,8 +1434,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
                        features, signing_pubkey,
                };
 
-               match offer_tlv_stream.node_id {
-                       Some(expected_signing_pubkey) => {
+               match (offer_tlv_stream.node_id, &offer_tlv_stream.paths) {
+                       (Some(expected_signing_pubkey), _) => {
                                if fields.signing_pubkey != expected_signing_pubkey {
                                        return Err(Bolt12SemanticError::InvalidSigningPubkey);
                                }
@@ -1445,7 +1445,21 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
                                )?;
                                Ok(InvoiceContents::ForOffer { invoice_request, fields })
                        },
-                       None => {
+                       (None, Some(paths)) => {
+                               if !paths
+                                       .iter()
+                                       .filter_map(|path| path.blinded_hops.last())
+                                       .any(|last_hop| fields.signing_pubkey == last_hop.blinded_node_id)
+                               {
+                                       return Err(Bolt12SemanticError::InvalidSigningPubkey);
+                               }
+
+                               let invoice_request = InvoiceRequestContents::try_from(
+                                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+                               )?;
+                               Ok(InvoiceContents::ForOffer { invoice_request, fields })
+                       },
+                       (None, None) => {
                                let refund = RefundContents::try_from(
                                        (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
                                )?;
@@ -1463,7 +1477,7 @@ mod tests {
        use bitcoin::blockdata::script::ScriptBuf;
        use bitcoin::hashes::Hash;
        use bitcoin::network::constants::Network;
-       use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self};
+       use bitcoin::secp256k1::{KeyPair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self};
        use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion};
        use bitcoin::key::TweakedPublicKey;
 
@@ -2366,6 +2380,81 @@ mod tests {
                }
        }
 
+       #[test]
+       fn parses_invoice_with_node_id_from_blinded_path() {
+               let paths = vec![
+                       BlindedPath {
+                               introduction_node: IntroductionNode::NodeId(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: IntroductionNode::NodeId(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 blinded_node_id_sign = |message: &UnsignedBolt12Invoice| {
+                       let secp_ctx = Secp256k1::new();
+                       let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap());
+                       Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
+               };
+
+               let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .clear_signing_pubkey()
+                       .amount_msats(1000)
+                       .path(paths[0].clone())
+                       .path(paths[1].clone())
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap()
+                       .respond_with_no_std_using_signing_pubkey(
+                               payment_paths(), payment_hash(), now(), pubkey(46)
+                       ).unwrap()
+                       .build().unwrap()
+                       .sign(blinded_node_id_sign).unwrap();
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               if let Err(e) = Bolt12Invoice::try_from(buffer) {
+                       panic!("error parsing invoice: {:?}", e);
+               }
+
+               let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .clear_signing_pubkey()
+                       .amount_msats(1000)
+                       .path(paths[0].clone())
+                       .path(paths[1].clone())
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap()
+                       .respond_with_no_std_using_signing_pubkey(
+                               payment_paths(), payment_hash(), now(), recipient_pubkey()
+                       ).unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               match Bolt12Invoice::try_from(buffer) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey));
+                       },
+               }
+       }
+
        #[test]
        fn fails_parsing_invoice_without_signature() {
                let mut buffer = Vec::new();
index be4696be91c7ee4bba7f105c13faa0f5c687b964..60d1e3c02a28b5d721e56ff88a3e7c36c5c42061 100644 (file)
@@ -754,6 +754,21 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (
 
                <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
        }
+
+       #[cfg(test)]
+       #[allow(dead_code)]
+       pub(super) fn respond_with_no_std_using_signing_pubkey(
+               &$self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
+               created_at: core::time::Duration, signing_pubkey: PublicKey
+       ) -> Result<$builder, Bolt12SemanticError> {
+               debug_assert!($contents.contents.inner.offer.signing_pubkey().is_none());
+
+               if $contents.invoice_request_features().requires_unknown_bits() {
+                       return Err(Bolt12SemanticError::UnknownRequiredFeatures);
+               }
+
+               <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
+       }
 } }
 
 macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => {