From c17d6775f1923d952aecd0059383f697a2406087 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_nonce 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 | 35 +++++++++++++++++++---- lightning/src/offers/signer.rs | 22 +++++++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 3b45f9511b5..7627b20c664 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_nonce< + #[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_nonce( + &$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 311ae2d99a1..6f80597a192 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -912,18 +912,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_nonce( + &self, bytes: &[u8], nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + self.verify_using_metadata(bytes, Some(&Metadata::Nonce(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, } }); @@ -932,7 +942,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); @@ -1295,6 +1305,11 @@ mod tests { Err(_) => panic!("unexpected error"), } + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(invoice_request.verify_using_nonce(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); @@ -1356,6 +1371,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_nonce(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 50016a051bd..502bc2df7f6 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. + Nonce(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::Nonce(_) => 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::Nonce(_) => 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::Nonce(_) => 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::Nonce(_) => 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::Nonce(_) => 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::Nonce(_) => (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::Nonce(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::Nonce(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::Nonce(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => false, }