]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add InvoiceRequest::verify_using_recipient_data
authorJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Jun 2024 00:18:26 +0000 (19:18 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Mon, 22 Jul 2024 16:34:02 +0000 (11:34 -0500)
Invoice requests are authenticated by checking the metadata in the
corresponding offer. For offers using blinded paths, this will simply be
a 128-bit nonce. Allows checking this nonce explicitly instead of the
metadata. This will be used by an upcoming change that includes the
nonce in the offer's blinded paths instead of the metadata, which
mitigate de-anonymization attacks.

lightning/src/offers/invoice_request.rs
lightning/src/offers/offer.rs
lightning/src/offers/signer.rs

index 3b45f9511b5fd4afef42c5a5970744493534523c..8c09721f28203793cf512f88c23a1541a718c5b4 100644 (file)
@@ -774,9 +774,11 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (
 } }
 
 macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => {
-       /// Verifies that the request was for an offer created using the given key. Returns the verified
-       /// request which contains the derived keys needed to sign a [`Bolt12Invoice`] for the request
-       /// if they could be extracted from the metadata.
+       /// Verifies that the request was for an offer created using the given key by checking the
+       /// metadata from the offer.
+       ///
+       /// Returns the verified request which contains the derived keys needed to sign a
+       /// [`Bolt12Invoice`] for the request if they could be extracted from the metadata.
        ///
        /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
        pub fn verify<
@@ -800,6 +802,35 @@ macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => {
                })
        }
 
+       /// Verifies that the request was for an offer created using the given key by checking a nonce
+       /// included with the [`BlindedPath`] for which the request was sent through.
+       ///
+       /// Returns the verified request which contains the derived keys needed to sign a
+       /// [`Bolt12Invoice`] for the request if they could be extracted from the metadata.
+       ///
+       /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
+       pub fn verify_using_recipient_data<
+               #[cfg(not(c_bindings))]
+               T: secp256k1::Signing
+       >(
+               $self: $self_type, nonce: Nonce, key: &ExpandedKey,
+               #[cfg(not(c_bindings))]
+               secp_ctx: &Secp256k1<T>,
+               #[cfg(c_bindings)]
+               secp_ctx: &Secp256k1<secp256k1::All>,
+       ) -> Result<VerifiedInvoiceRequest, ()> {
+               let (offer_id, keys) = $self.contents.inner.offer.verify_using_recipient_data(
+                       &$self.bytes, nonce, key, secp_ctx
+               )?;
+               Ok(VerifiedInvoiceRequest {
+                       offer_id,
+                       #[cfg(not(c_bindings))]
+                       inner: $self,
+                       #[cfg(c_bindings)]
+                       inner: $self.clone(),
+                       keys,
+               })
+       }
 } }
 
 #[cfg(not(c_bindings))]
index 674cfeaa12ceab6817be76a7a86a9308241da783..6ac97a2d74980de4b9852bf439d4bdf903461979 100644 (file)
@@ -913,18 +913,28 @@ impl OfferContents {
                self.signing_pubkey
        }
 
-       /// Verifies that the offer metadata was produced from the offer in the TLV stream.
        pub(super) fn verify<T: secp256k1::Signing>(
                &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1<T>
        ) -> Result<(OfferId, Option<Keypair>), ()> {
-               match self.metadata() {
+               self.verify_using_metadata(bytes, self.metadata.as_ref(), key, secp_ctx)
+       }
+
+       pub(super) fn verify_using_recipient_data<T: secp256k1::Signing>(
+               &self, bytes: &[u8], nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> Result<(OfferId, Option<Keypair>), ()> {
+               self.verify_using_metadata(bytes, Some(&Metadata::RecipientData(nonce)), key, secp_ctx)
+       }
+
+       /// Verifies that the offer metadata was produced from the offer in the TLV stream.
+       fn verify_using_metadata<T: secp256k1::Signing>(
+               &self, bytes: &[u8], metadata: Option<&Metadata>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> Result<(OfferId, Option<Keypair>), ()> {
+               match 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_recipient_keys()
-                                               },
+                                               OFFER_NODE_ID_TYPE => !metadata.derives_recipient_keys(),
                                                _ => true,
                                        }
                                });
@@ -933,7 +943,7 @@ impl OfferContents {
                                        None => return Err(()),
                                };
                                let keys = signer::verify_recipient_metadata(
-                                       metadata, key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx
+                                       metadata.as_ref(), key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx
                                )?;
 
                                let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes);
@@ -1296,6 +1306,14 @@ mod tests {
                        Err(_) => panic!("unexpected error"),
                }
 
+               // Fails verification when using the wrong method
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(
+                       invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).is_err()
+               );
+
                // Fails verification with altered offer field
                let mut tlv_stream = offer.as_tlv_stream();
                tlv_stream.amount = Some(100);
@@ -1357,6 +1375,14 @@ mod tests {
                        Err(_) => panic!("unexpected error"),
                }
 
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               match invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx) {
+                       Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()),
+                       Err(_) => panic!("unexpected error"),
+               }
+
                // Fails verification with altered offer field
                let mut tlv_stream = offer.as_tlv_stream();
                tlv_stream.amount = Some(100);
index 50016a051bd2672f16f8233f64fdc57b7a23575e..25b4c52865d74d7d9c72e8eae825a53b2da2c752 100644 (file)
@@ -43,6 +43,9 @@ pub(super) enum Metadata {
        /// Metadata as parsed, supplied by the user, or derived from the message contents.
        Bytes(Vec<u8>),
 
+       /// Metadata for deriving keys included as recipient data in a blinded path.
+       RecipientData(Nonce),
+
        /// Metadata to be derived from message contents and given material.
        Derived(MetadataMaterial),
 
@@ -54,6 +57,7 @@ impl Metadata {
        pub fn as_bytes(&self) -> Option<&Vec<u8>> {
                match self {
                        Metadata::Bytes(bytes) => Some(bytes),
+                       Metadata::RecipientData(_) => None,
                        Metadata::Derived(_) => None,
                        Metadata::DerivedSigningPubkey(_) => None,
                }
@@ -62,6 +66,7 @@ impl Metadata {
        pub fn has_derivation_material(&self) -> bool {
                match self {
                        Metadata::Bytes(_) => false,
+                       Metadata::RecipientData(_) => false,
                        Metadata::Derived(_) => true,
                        Metadata::DerivedSigningPubkey(_) => true,
                }
@@ -75,6 +80,7 @@ impl Metadata {
                        // derived, as wouldn't be the case if a Metadata::Bytes with length PaymentId::LENGTH +
                        // Nonce::LENGTH had been set explicitly.
                        Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH,
+                       Metadata::RecipientData(_) => false,
                        Metadata::Derived(_) => false,
                        Metadata::DerivedSigningPubkey(_) => true,
                }
@@ -88,6 +94,7 @@ impl Metadata {
                        // 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::RecipientData(_) => true,
                        Metadata::Derived(_) => false,
                        Metadata::DerivedSigningPubkey(_) => true,
                }
@@ -96,6 +103,7 @@ impl Metadata {
        pub fn without_keys(self) -> Self {
                match self {
                        Metadata::Bytes(_) => self,
+                       Metadata::RecipientData(_) => self,
                        Metadata::Derived(_) => self,
                        Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material),
                }
@@ -106,6 +114,7 @@ impl Metadata {
        ) -> (Self, Option<Keypair>) {
                match self {
                        Metadata::Bytes(_) => (self, None),
+                       Metadata::RecipientData(_) => (self, None),
                        Metadata::Derived(mut metadata_material) => {
                                tlv_stream.write(&mut metadata_material.hmac).unwrap();
                                (Metadata::Bytes(metadata_material.derive_metadata()), None)
@@ -126,10 +135,22 @@ impl Default for Metadata {
        }
 }
 
+impl AsRef<[u8]> for Metadata {
+       fn as_ref(&self) -> &[u8] {
+               match self {
+                       Metadata::Bytes(bytes) => &bytes,
+                       Metadata::RecipientData(nonce) => &nonce.0,
+                       Metadata::Derived(_) => &[],
+                       Metadata::DerivedSigningPubkey(_) => &[],
+               }
+       }
+}
+
 impl fmt::Debug for Metadata {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                match self {
                        Metadata::Bytes(bytes) => bytes.fmt(f),
+                       Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f),
                        Metadata::Derived(_) => f.write_str("Derived"),
                        Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"),
                }
@@ -145,6 +166,7 @@ impl PartialEq for Metadata {
                        } else {
                                false
                        },
+                       Metadata::RecipientData(_) => false,
                        Metadata::Derived(_) => false,
                        Metadata::DerivedSigningPubkey(_) => false,
                }