From e15641504894d7f6e0c0492db569a13d117eb084 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 19 Jun 2024 19:18:26 -0500 Subject: [PATCH] Add InvoiceRequest::verify_using_recipient_data 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 | 37 ++++++++++++++++++++++-- lightning/src/offers/offer.rs | 38 +++++++++++++++++++++---- lightning/src/offers/signer.rs | 22 ++++++++++++++ 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 3b45f9511..8c09721f2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -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, + #[cfg(c_bindings)] + secp_ctx: &Secp256k1, + ) -> Result { + 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))] diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 674cfeaa1..6ac97a2d7 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -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( &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result<(OfferId, Option), ()> { - match self.metadata() { + self.verify_using_metadata(bytes, self.metadata.as_ref(), key, secp_ctx) + } + + pub(super) fn verify_using_recipient_data( + &self, bytes: &[u8], nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + 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( + &self, bytes: &[u8], metadata: Option<&Metadata>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + 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); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 50016a051..25b4c5286 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -43,6 +43,9 @@ pub(super) enum Metadata { /// Metadata as parsed, supplied by the user, or derived from the message contents. Bytes(Vec), + /// 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> { 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) { 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, } -- 2.39.5