From cc7c19f4409c9216ca7ffc7bd952d50020b49813 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 00:43:04 +0000 Subject: [PATCH 01/11] Add DNS(SEC) query and proof messages and onion message handler This creates the initial DNSSEC proof and query messages in a new module in `onion_message`, as well as a new message handler to handle them. In the coming commits, a default implementation will be added which verifies DNSSEC proofs which can be used to resolve BIP 353 URIs without relying on anything outside of the lightning network. --- lightning/Cargo.toml | 2 + lightning/src/onion_message/dns_resolution.rs | 155 ++++++++++++++++++ lightning/src/onion_message/mod.rs | 1 + 3 files changed, 158 insertions(+) create mode 100644 lightning/src/onion_message/dns_resolution.rs diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 2f8d284116b..478b686e893 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -44,6 +44,8 @@ lightning-invoice = { version = "0.32.0-rc1", path = "../lightning-invoice", def bech32 = { version = "0.9.1", default-features = false } bitcoin = { version = "0.32.2", default-features = false, features = ["secp-recovery"] } +dnssec-prover = { version = "0.6", default-features = false } + hashbrown = { version = "0.13", optional = true, default-features = false } possiblyrandom = { version = "0.2", path = "../possiblyrandom", optional = true, default-features = false } regex = { version = "1.5.6", optional = true } diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs new file mode 100644 index 00000000000..d8c4089f8db --- /dev/null +++ b/lightning/src/onion_message/dns_resolution.rs @@ -0,0 +1,155 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! This module defines message handling for DNSSEC proof fetching using [bLIP 32]. +//! +//! It contains [`DNSResolverMessage`]s as well as a [`DNSResolverMessageHandler`] trait to handle +//! such messages using an [`OnionMessenger`]. +//! +//! [bLIP 32]: https://github.com/lightning/blips/blob/master/blip-0032.md +//! [`OnionMessenger`]: super::messenger::OnionMessenger + +use dnssec_prover::rr::Name; + +use crate::io; +use crate::ln::msgs::DecodeError; +use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; +use crate::onion_message::packet::OnionMessageContents; +use crate::prelude::*; +use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; + +/// A handler for an [`OnionMessage`] containing a DNS(SEC) query or a DNSSEC proof +/// +/// [`OnionMessage`]: crate::ln::msgs::OnionMessage +pub trait DNSResolverMessageHandler { + /// Handle a [`DNSSECQuery`] message. + /// + /// If we provide DNS resolution services to third parties, we should respond with a + /// [`DNSSECProof`] message. + fn dnssec_query( + &self, message: DNSSECQuery, responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)>; + + /// Handle a [`DNSSECProof`] message (in response to a [`DNSSECQuery`] we presumably sent). + /// + /// With this, we should be able to validate the DNS record we requested. + fn dnssec_proof(&self, message: DNSSECProof); + + /// Release any [`DNSResolverMessage`]s that need to be sent. + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + vec![] + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +/// An enum containing the possible onion messages which are used uses to request and recieve +/// DNSSEC proofs. +pub enum DNSResolverMessage { + /// A query requesting a DNSSEC proof + DNSSECQuery(DNSSECQuery), + /// A response containing a DNSSEC proof + DNSSECProof(DNSSECProof), +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +/// A message which is sent to a DNSSEC prover requesting a DNSSEC proof for the given name. +pub struct DNSSECQuery(pub Name); +const DNSSEC_QUERY_TYPE: u64 = 65536; + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +/// A message which is sent in response to [`DNSSECQuery`] containing a DNSSEC proof. +pub struct DNSSECProof { + /// The name which the query was for. The proof may not contain a DNS RR for exactly this name + /// if it contains a wildcard RR which contains this name instead. + pub name: Name, + /// The RFC 9102-formatted DNSSEC proof. + pub proof: Vec, +} +const DNSSEC_PROOF_TYPE: u64 = 65538; + +impl DNSResolverMessage { + /// Returns whether `tlv_type` corresponds to a TLV record for DNS Resolvers. + pub fn is_known_type(tlv_type: u64) -> bool { + match tlv_type { + DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE => true, + _ => false, + } + } +} + +impl Writeable for DNSResolverMessage { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + match self { + Self::DNSSECQuery(DNSSECQuery(q)) => { + (q.as_str().len() as u8).write(w)?; + w.write_all(&q.as_str().as_bytes()) + }, + Self::DNSSECProof(DNSSECProof { name, proof }) => { + (name.as_str().len() as u8).write(w)?; + w.write_all(&name.as_str().as_bytes())?; + proof.write(w) + }, + } + } +} + +fn read_byte_len_ascii_string(buffer: &mut R) -> Result { + let len: u8 = Readable::read(buffer)?; + let mut bytes = [0; 255]; + buffer.read_exact(&mut bytes[..len as usize])?; + if bytes[..len as usize].iter().any(|b| *b < 0x20 || *b > 0x7e) { + // If the bytes are not entirely in the printable ASCII range, fail + return Err(DecodeError::InvalidValue); + } + let s = + String::from_utf8(bytes[..len as usize].to_vec()).map_err(|_| DecodeError::InvalidValue)?; + Ok(s) +} + +impl ReadableArgs for DNSResolverMessage { + fn read(r: &mut R, message_type: u64) -> Result { + match message_type { + DNSSEC_QUERY_TYPE => { + let s = read_byte_len_ascii_string(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + Ok(DNSResolverMessage::DNSSECQuery(DNSSECQuery(name))) + }, + DNSSEC_PROOF_TYPE => { + let s = read_byte_len_ascii_string(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + let proof = Readable::read(r)?; + Ok(DNSResolverMessage::DNSSECProof(DNSSECProof { name, proof })) + }, + _ => Err(DecodeError::InvalidValue), + } + } +} + +impl OnionMessageContents for DNSResolverMessage { + #[cfg(c_bindings)] + fn msg_type(&self) -> String { + match self { + DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query".to_string(), + DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof".to_string(), + } + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + match self { + DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query", + DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof", + } + } + fn tlv_type(&self) -> u64 { + match self { + DNSResolverMessage::DNSSECQuery(_) => DNSSEC_QUERY_TYPE, + DNSResolverMessage::DNSSECProof(_) => DNSSEC_PROOF_TYPE, + } + } +} diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index 8cdf098a3e5..a5735e372f3 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -22,6 +22,7 @@ //! [`OnionMessenger`]: self::messenger::OnionMessenger pub mod async_payments; +pub mod dns_resolution; pub mod messenger; pub mod offers; pub mod packet; From 6b760e73d124c4afeda80bb9bfe69646ce0abd81 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:31:53 +0000 Subject: [PATCH 02/11] f sp --- lightning/src/onion_message/dns_resolution.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index d8c4089f8db..8e25ebc0931 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -48,7 +48,7 @@ pub trait DNSResolverMessageHandler { } #[derive(Clone, Debug, Hash, PartialEq, Eq)] -/// An enum containing the possible onion messages which are used uses to request and recieve +/// An enum containing the possible onion messages which are used uses to request and receive /// DNSSEC proofs. pub enum DNSResolverMessage { /// A query requesting a DNSSEC proof From 513bb88bd3f0c0c0bce14a1721fbbe06f2043ba3 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:42:30 +0000 Subject: [PATCH 03/11] f use hostname type --- lightning/src/onion_message/dns_resolution.rs | 19 +++---------------- lightning/src/util/ser.rs | 10 ++++++++++ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 8e25ebc0931..2e3bd36b08b 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -22,7 +22,7 @@ use crate::ln::msgs::DecodeError; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; -use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; +use crate::util::ser::{Hostname, Readable, ReadableArgs, Writeable, Writer}; /// A handler for an [`OnionMessage`] containing a DNS(SEC) query or a DNSSEC proof /// @@ -99,29 +99,16 @@ impl Writeable for DNSResolverMessage { } } -fn read_byte_len_ascii_string(buffer: &mut R) -> Result { - let len: u8 = Readable::read(buffer)?; - let mut bytes = [0; 255]; - buffer.read_exact(&mut bytes[..len as usize])?; - if bytes[..len as usize].iter().any(|b| *b < 0x20 || *b > 0x7e) { - // If the bytes are not entirely in the printable ASCII range, fail - return Err(DecodeError::InvalidValue); - } - let s = - String::from_utf8(bytes[..len as usize].to_vec()).map_err(|_| DecodeError::InvalidValue)?; - Ok(s) -} - impl ReadableArgs for DNSResolverMessage { fn read(r: &mut R, message_type: u64) -> Result { match message_type { DNSSEC_QUERY_TYPE => { - let s = read_byte_len_ascii_string(r)?; + let s = Hostname::read(r)?; let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; Ok(DNSResolverMessage::DNSSECQuery(DNSSECQuery(name))) }, DNSSEC_PROOF_TYPE => { - let s = read_byte_len_ascii_string(r)?; + let s = Hostname::read(r)?; let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; let proof = Readable::read(r)?; Ok(DNSResolverMessage::DNSSECProof(DNSSECProof { name, proof })) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 1c48463b89f..a032291c4ae 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -37,6 +37,9 @@ use bitcoin::hashes::hmac::Hmac; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hash_types::{Txid, BlockHash}; + +use dnssec_prover::rr::Name; + use core::time::Duration; use crate::chain::ClaimId; use crate::ln::msgs::DecodeError; @@ -1551,6 +1554,13 @@ impl Readable for Hostname { } } +impl TryInto for Hostname { + type Error = (); + fn try_into(self) -> Result { + Name::try_from(self.0) + } +} + /// This is not exported to bindings users as `Duration`s are simply mapped as ints. impl Writeable for Duration { #[inline] From e1a87afeb4fa63c3deece734d32467b125184d67 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 17:08:27 +0000 Subject: [PATCH 04/11] Parse and handle `DNSResolverMessage`s in `OnionMessenger` This adds the requisite message parsing and handling code for the new DNSSEC messages to `OnionMessenger`. --- fuzz/src/onion_message.rs | 2 + lightning-background-processor/src/lib.rs | 4 +- lightning/src/ln/functional_test_utils.rs | 2 + lightning/src/ln/offers_tests.rs | 12 +--- lightning/src/ln/peer_handler.rs | 9 +++ .../src/onion_message/functional_tests.rs | 20 +++++- lightning/src/onion_message/messenger.rs | 71 ++++++++++++++----- lightning/src/onion_message/packet.rs | 18 ++++- 8 files changed, 105 insertions(+), 33 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 33458436c66..99305a38fb3 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -9,6 +9,7 @@ use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, Offer use lightning::blinded_path::EmptyNodeIdLookUp; use lightning::ln::features::InitFeatures; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; +use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::script::ShutdownScript; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::UnsignedInvoiceRequest; @@ -54,6 +55,7 @@ pub fn do_test(data: &[u8], logger: &L) { &message_router, &offers_msg_handler, &async_payments_msg_handler, + IgnoringMessageHandler {}, // TODO: Move to ChannelManager once it supports DNSSEC. &custom_msg_handler, ); diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index bbe1545d044..a3a1aca48cf 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -648,7 +648,7 @@ use futures_util::{dummy_waker, OptionalSelector, Selector, SelectorOutput}; /// # type NetworkGraph = lightning::routing::gossip::NetworkGraph>; /// # type P2PGossipSync
    = lightning::routing::gossip::P2PGossipSync, Arc
      , Arc>; /// # type ChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager, B, FE, Logger>; -/// # type OnionMessenger = lightning::onion_message::messenger::OnionMessenger, Arc, Arc, Arc>, Arc, Arc, Arc>>, Arc>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>; +/// # type OnionMessenger = lightning::onion_message::messenger::OnionMessenger, Arc, Arc, Arc>, Arc, Arc, Arc>>, Arc>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>; /// # type Scorer = RwLock, Arc>>; /// # type PeerManager = lightning::ln::peer_handler::SimpleArcPeerManager, B, FE, Arc
        , Logger>; /// # @@ -1186,6 +1186,7 @@ mod tests { IgnoringMessageHandler, Arc, IgnoringMessageHandler, + IgnoringMessageHandler, >; struct Node { @@ -1587,6 +1588,7 @@ mod tests { IgnoringMessageHandler {}, manager.clone(), IgnoringMessageHandler {}, + IgnoringMessageHandler {}, )); let wallet = Arc::new(TestWallet {}); let sweeper = Arc::new(OutputSweeper::new( diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index f3290bf1583..6be39e85850 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -414,6 +414,7 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) IgnoringMessageHandler, >; @@ -3265,6 +3266,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec( OffersMessage::StaticInvoice(invoice) => panic!("Unexpected static invoice: {:?}", invoice), OffersMessage::InvoiceError(error) => panic!("Unexpected invoice_error: {:?}", error), }, - #[cfg(async_payments)] - ParsedOnionMessageContents::AsyncPayments(message) => panic!("Unexpected async payments message: {:?}", message), - ParsedOnionMessageContents::Custom(message) => panic!("Unexpected custom message: {:?}", message), + _ => panic!("Unexpected message {:?}", message), }, Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), Err(e) => panic!("Failed to process onion message {:?}", e), @@ -229,9 +227,7 @@ fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) OffersMessage::StaticInvoice(invoice) => panic!("Unexpected static invoice: {:?}", invoice), OffersMessage::InvoiceError(error) => panic!("Unexpected invoice_error: {:?}", error), }, - #[cfg(async_payments)] - ParsedOnionMessageContents::AsyncPayments(message) => panic!("Unexpected async payments message: {:?}", message), - ParsedOnionMessageContents::Custom(message) => panic!("Unexpected custom message: {:?}", message), + _ => panic!("Unexpected message {:?}", message), }, Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), Err(e) => panic!("Failed to process onion message {:?}", e), @@ -250,9 +246,7 @@ fn extract_invoice_error<'a, 'b, 'c>( OffersMessage::StaticInvoice(invoice) => panic!("Unexpected invoice: {:?}", invoice), OffersMessage::InvoiceError(error) => error, }, - #[cfg(async_payments)] - ParsedOnionMessageContents::AsyncPayments(message) => panic!("Unexpected async payments message: {:?}", message), - ParsedOnionMessageContents::Custom(message) => panic!("Unexpected custom message: {:?}", message), + _ => panic!("Unexpected message: {:?}", message), }, Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), Err(e) => panic!("Failed to process onion message {:?}", e), diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 51695bba09a..5e1419fd0dd 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -30,6 +30,7 @@ use crate::ln::peer_channel_encryptor::{PeerChannelEncryptor, NextNoiseStep, Mes use crate::ln::wire; use crate::ln::wire::{Encode, Type}; use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use crate::onion_message::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery}; use crate::onion_message::messenger::{CustomOnionMessageHandler, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::onion_message::packet::OnionMessageContents; @@ -154,6 +155,14 @@ impl AsyncPaymentsMessageHandler for IgnoringMessageHandler { } fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} } +impl DNSResolverMessageHandler for IgnoringMessageHandler { + fn dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + fn dnssec_proof(&self, _message: DNSSECProof) {} +} impl CustomOnionMessageHandler for IgnoringMessageHandler { type CustomMessage = Infallible; fn handle_custom_message(&self, _message: Infallible, _context: Option>, _responder: Option) -> Option<(Infallible, ResponseInstruction)> { diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 56c54005739..7639f003b86 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -20,6 +20,7 @@ use crate::sign::{NodeSigner, Recipient}; use crate::util::ser::{FixedLengthReader, LengthReadable, Writeable, Writer}; use crate::util::test_utils; use super::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use super::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery}; use super::messenger::{CustomOnionMessageHandler, DefaultMessageRouter, Destination, OnionMessagePath, OnionMessenger, Responder, ResponseInstruction, MessageSendInstructions, SendError, SendSuccess}; use super::offers::{OffersMessage, OffersMessageHandler}; use super::packet::{OnionMessageContents, Packet}; @@ -52,6 +53,7 @@ struct MessengerNode { >>, Arc, Arc, + Arc, Arc >, custom_message_handler: Arc, @@ -90,6 +92,17 @@ impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} } +struct TestDNSResolverMessageHandler {} + +impl DNSResolverMessageHandler for TestDNSResolverMessageHandler { + fn dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + fn dnssec_proof(&self, _message: DNSSECProof) {} +} + #[derive(Clone, Debug, PartialEq)] enum TestCustomMessage { Ping, @@ -264,18 +277,21 @@ fn create_nodes_using_cfgs(cfgs: Vec) -> Vec { ); let offers_message_handler = Arc::new(TestOffersMessageHandler {}); let async_payments_message_handler = Arc::new(TestAsyncPaymentsMessageHandler {}); + let dns_resolver_message_handler = Arc::new(TestDNSResolverMessageHandler {}); let custom_message_handler = Arc::new(TestCustomMessageHandler::new()); let messenger = if cfg.intercept_offline_peer_oms { OnionMessenger::new_with_offline_peer_interception( entropy_source.clone(), node_signer.clone(), logger.clone(), node_id_lookup, message_router, offers_message_handler, - async_payments_message_handler, custom_message_handler.clone() + async_payments_message_handler, dns_resolver_message_handler, + custom_message_handler.clone(), ) } else { OnionMessenger::new( entropy_source.clone(), node_signer.clone(), logger.clone(), node_id_lookup, message_router, offers_message_handler, - async_payments_message_handler, custom_message_handler.clone() + async_payments_message_handler, dns_resolver_message_handler, + custom_message_handler.clone(), ) }; nodes.push(MessengerNode { diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f40a59f2c0c..10001bdde29 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -27,6 +27,7 @@ use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; use super::async_payments::AsyncPaymentsMessageHandler; #[cfg(async_payments)] use super::async_payments::AsyncPaymentsMessage; +use super::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage}; use super::packet::OnionMessageContents; use super::packet::ParsedOnionMessageContents; use super::offers::OffersMessageHandler; @@ -86,16 +87,20 @@ pub trait AOnionMessenger { type AsyncPaymentsMessageHandler: AsyncPaymentsMessageHandler + ?Sized; /// A type that may be dereferenced to [`Self::AsyncPaymentsMessageHandler`] type APH: Deref; + /// A type implementing [`DNSResolverMessageHandler`] + type DNSResolverMessageHandler: DNSResolverMessageHandler + ?Sized; + /// A type that may be dereferenced to [`Self::DNSResolverMessageHandler`] + type DRH: Deref; /// A type implementing [`CustomOnionMessageHandler`] type CustomOnionMessageHandler: CustomOnionMessageHandler + ?Sized; /// A type that may be dereferenced to [`Self::CustomOnionMessageHandler`] type CMH: Deref; /// Returns a reference to the actual [`OnionMessenger`] object. - fn get_om(&self) -> &OnionMessenger; + fn get_om(&self) -> &OnionMessenger; } -impl AOnionMessenger -for OnionMessenger where +impl AOnionMessenger +for OnionMessenger where ES::Target: EntropySource, NS::Target: NodeSigner, L::Target: Logger, @@ -103,6 +108,7 @@ for OnionMessenger where MR::Target: MessageRouter, OMH::Target: OffersMessageHandler, APH:: Target: AsyncPaymentsMessageHandler, + DRH::Target: DNSResolverMessageHandler, CMH::Target: CustomOnionMessageHandler, { type EntropySource = ES::Target; @@ -119,9 +125,11 @@ for OnionMessenger where type OMH = OMH; type AsyncPaymentsMessageHandler = APH::Target; type APH = APH; + type DNSResolverMessageHandler = DRH::Target; + type DRH = DRH; type CustomOnionMessageHandler = CMH::Target; type CMH = CMH; - fn get_om(&self) -> &OnionMessenger { self } + fn get_om(&self) -> &OnionMessenger { self } } /// A sender, receiver and forwarder of [`OnionMessage`]s. @@ -194,11 +202,13 @@ for OnionMessenger where /// # let custom_message_handler = IgnoringMessageHandler {}; /// # let offers_message_handler = IgnoringMessageHandler {}; /// # let async_payments_message_handler = IgnoringMessageHandler {}; +/// # let dns_resolution_message_handler = IgnoringMessageHandler {}; /// // Create the onion messenger. This must use the same `keys_manager` as is passed to your /// // ChannelManager. /// let onion_messenger = OnionMessenger::new( /// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router, -/// &offers_message_handler, &async_payments_message_handler, &custom_message_handler +/// &offers_message_handler, &async_payments_message_handler, &dns_resolution_message_handler, +/// &custom_message_handler, /// ); /// # #[derive(Debug)] @@ -241,7 +251,7 @@ for OnionMessenger where /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub struct OnionMessenger< - ES: Deref, NS: Deref, L: Deref, NL: Deref, MR: Deref, OMH: Deref, APH: Deref, CMH: Deref + ES: Deref, NS: Deref, L: Deref, NL: Deref, MR: Deref, OMH: Deref, APH: Deref, DRH: Deref, CMH: Deref > where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -250,6 +260,7 @@ pub struct OnionMessenger< MR::Target: MessageRouter, OMH::Target: OffersMessageHandler, APH::Target: AsyncPaymentsMessageHandler, + DRH::Target: DNSResolverMessageHandler, CMH::Target: CustomOnionMessageHandler, { entropy_source: ES, @@ -262,6 +273,7 @@ pub struct OnionMessenger< offers_handler: OMH, #[allow(unused)] async_payments_handler: APH, + dns_resolver_handler: DRH, custom_handler: CMH, intercept_messages_for_offline_peers: bool, pending_intercepted_msgs_events: Mutex>, @@ -1067,8 +1079,8 @@ macro_rules! drop_handled_events_and_abort { ($self: expr, $res_iter: expr, $eve } }} -impl -OnionMessenger +impl +OnionMessenger where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1077,17 +1089,18 @@ where MR::Target: MessageRouter, OMH::Target: OffersMessageHandler, APH::Target: AsyncPaymentsMessageHandler, + DRH::Target: DNSResolverMessageHandler, CMH::Target: CustomOnionMessageHandler, { /// Constructs a new `OnionMessenger` to send, forward, and delegate received onion messages to /// their respective handlers. pub fn new( entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, message_router: MR, - offers_handler: OMH, async_payments_handler: APH, custom_handler: CMH + offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, custom_handler: CMH, ) -> Self { Self::new_inner( entropy_source, node_signer, logger, node_id_lookup, message_router, - offers_handler, async_payments_handler, custom_handler, false + offers_handler, async_payments_handler, dns_resolver, custom_handler, false, ) } @@ -1114,18 +1127,19 @@ where /// peers. pub fn new_with_offline_peer_interception( entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, - message_router: MR, offers_handler: OMH, async_payments_handler: APH, custom_handler: CMH + message_router: MR, offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, + custom_handler: CMH, ) -> Self { Self::new_inner( entropy_source, node_signer, logger, node_id_lookup, message_router, - offers_handler, async_payments_handler, custom_handler, true + offers_handler, async_payments_handler, dns_resolver, custom_handler, true, ) } fn new_inner( entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, - message_router: MR, offers_handler: OMH, async_payments_handler: APH, custom_handler: CMH, - intercept_messages_for_offline_peers: bool + message_router: MR, offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, + custom_handler: CMH, intercept_messages_for_offline_peers: bool, ) -> Self { let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); @@ -1139,6 +1153,7 @@ where message_router, offers_handler, async_payments_handler, + dns_resolver_handler: dns_resolver, custom_handler, intercept_messages_for_offline_peers, pending_intercepted_msgs_events: Mutex::new(Vec::new()), @@ -1475,8 +1490,8 @@ fn outbound_buffer_full(peer_node_id: &PublicKey, buffer: &HashMap EventsProvider -for OnionMessenger +impl EventsProvider +for OnionMessenger where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1485,6 +1500,7 @@ where MR::Target: MessageRouter, OMH::Target: OffersMessageHandler, APH::Target: AsyncPaymentsMessageHandler, + DRH::Target: DNSResolverMessageHandler, CMH::Target: CustomOnionMessageHandler, { fn process_pending_events(&self, handler: H) where H::Target: EventHandler { @@ -1560,8 +1576,8 @@ where } } -impl OnionMessageHandler -for OnionMessenger +impl OnionMessageHandler +for OnionMessenger where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1570,6 +1586,7 @@ where MR::Target: MessageRouter, OMH::Target: OffersMessageHandler, APH::Target: AsyncPaymentsMessageHandler, + DRH::Target: DNSResolverMessageHandler, CMH::Target: CustomOnionMessageHandler, { fn handle_onion_message(&self, peer_node_id: &PublicKey, msg: &OnionMessage) { @@ -1610,6 +1627,15 @@ where ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::ReleaseHeldHtlc(msg)) => { self.async_payments_handler.release_held_htlc(msg); }, + ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECQuery(msg)) => { + let response_instructions = self.dns_resolver_handler.dnssec_query(msg, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECProof(msg)) => { + self.dns_resolver_handler.dnssec_proof(msg); + }, ParsedOnionMessageContents::Custom(msg) => { let context = match context { None => None, @@ -1753,6 +1779,13 @@ where ); } + // Enqueue any initiating `DNSResolverMessage`s to send. + for (message, instructions) in self.dns_resolver_handler.release_pending_messages() { + let _ = self.send_onion_message_internal( + message, instructions, format_args!("when sending DNSResolverMessage") + ); + } + // Enqueue any initiating `CustomMessage`s to send. for (message, instructions) in self.custom_handler.release_pending_custom_messages() { let _ = self.send_onion_message_internal( @@ -1784,6 +1817,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>>, Arc, Arc>>, Arc>, Arc>, + IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) IgnoringMessageHandler >; @@ -1805,6 +1839,7 @@ pub type SimpleRefOnionMessenger< &'j DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, &'i SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, M, T, F, L>, &'i SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, M, T, F, L>, + IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) IgnoringMessageHandler >; diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index f65ed0d53ed..8ec85a6bed7 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -18,6 +18,7 @@ use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; #[cfg(async_payments)] use super::async_payments::AsyncPaymentsMessage; +use super::dns_resolution::DNSResolverMessage; use super::messenger::CustomOnionMessageHandler; use super::offers::OffersMessage; use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; @@ -132,6 +133,8 @@ pub enum ParsedOnionMessageContents { /// A message related to async payments. #[cfg(async_payments)] AsyncPayments(AsyncPaymentsMessage), + /// A message requesting or providing a DNSSEC proof + DNSResolver(DNSResolverMessage), /// A custom onion message specified by the user. Custom(T), } @@ -145,6 +148,7 @@ impl OnionMessageContents for ParsedOnionMessageContent &ParsedOnionMessageContents::Offers(ref msg) => msg.tlv_type(), #[cfg(async_payments)] &ParsedOnionMessageContents::AsyncPayments(ref msg) => msg.tlv_type(), + &ParsedOnionMessageContents::DNSResolver(ref msg) => msg.tlv_type(), &ParsedOnionMessageContents::Custom(ref msg) => msg.tlv_type(), } } @@ -154,6 +158,7 @@ impl OnionMessageContents for ParsedOnionMessageContent ParsedOnionMessageContents::Offers(ref msg) => msg.msg_type(), #[cfg(async_payments)] ParsedOnionMessageContents::AsyncPayments(ref msg) => msg.msg_type(), + ParsedOnionMessageContents::DNSResolver(ref msg) => msg.msg_type(), ParsedOnionMessageContents::Custom(ref msg) => msg.msg_type(), } } @@ -163,6 +168,7 @@ impl OnionMessageContents for ParsedOnionMessageContent ParsedOnionMessageContents::Offers(ref msg) => msg.msg_type(), #[cfg(async_payments)] ParsedOnionMessageContents::AsyncPayments(ref msg) => msg.msg_type(), + ParsedOnionMessageContents::DNSResolver(ref msg) => msg.msg_type(), ParsedOnionMessageContents::Custom(ref msg) => msg.msg_type(), } } @@ -171,10 +177,11 @@ impl OnionMessageContents for ParsedOnionMessageContent impl Writeable for ParsedOnionMessageContents { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { - ParsedOnionMessageContents::Offers(msg) => Ok(msg.write(w)?), + ParsedOnionMessageContents::Offers(msg) => msg.write(w), #[cfg(async_payments)] - ParsedOnionMessageContents::AsyncPayments(msg) => Ok(msg.write(w)?), - ParsedOnionMessageContents::Custom(msg) => Ok(msg.write(w)?), + ParsedOnionMessageContents::AsyncPayments(msg) => msg.write(w), + ParsedOnionMessageContents::DNSResolver(msg) => msg.write(w), + ParsedOnionMessageContents::Custom(msg) => msg.write(w), } } } @@ -286,6 +293,11 @@ for Payload::CustomM message = Some(ParsedOnionMessageContents::AsyncPayments(msg)); Ok(true) }, + tlv_type if DNSResolverMessage::is_known_type(tlv_type) => { + let msg = DNSResolverMessage::read(msg_reader, tlv_type)?; + message = Some(ParsedOnionMessageContents::DNSResolver(msg)); + Ok(true) + }, _ => match handler.read_custom_message(msg_type, msg_reader)? { Some(msg) => { message = Some(ParsedOnionMessageContents::Custom(msg)); From 4359c8900418177d93a925d5c98b5ae677bea403 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 18:38:32 +0000 Subject: [PATCH 05/11] Add a `MessageContext::DNSResolution` to protect against probing When we make a DNSSEC query with a reply path, we don't want to allow the DNS resolver to attempt to respond to various nodes to try to detect (through timining or other analysis) whether we were the one who made the query. Thus, we need to include a nonce in the context in our reply path, which we set up here by creating a new context type for DNS resolutions. --- lightning/src/blinded_path/message.rs | 24 +++++++++++++++++++ lightning/src/ln/peer_handler.rs | 4 ++-- lightning/src/onion_message/dns_resolution.rs | 3 ++- .../src/onion_message/functional_tests.rs | 4 ++-- lightning/src/onion_message/messenger.rs | 13 +++++++--- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index e3899b50edb..d3958ef3aa1 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -280,6 +280,11 @@ pub enum MessageContext { /// /// [`OffersMessage`]: crate::onion_message::offers::OffersMessage Offers(OffersContext), + /// Represents a context for a blinded path used in a reply path when requesting a DNSSEC proof + /// in a [`DNSResolverMessage`]. + /// + /// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage + DNSResolver(DNSResolverContext), /// Context specific to a [`CustomOnionMessageHandler::CustomMessage`]. /// /// [`CustomOnionMessageHandler::CustomMessage`]: crate::onion_message::messenger::CustomOnionMessageHandler::CustomMessage @@ -353,6 +358,7 @@ pub enum OffersContext { impl_writeable_tlv_based_enum!(MessageContext, {0, Offers} => (), {1, Custom} => (), + {2, DNSResolver} => (), ); impl_writeable_tlv_based_enum!(OffersContext, @@ -369,6 +375,24 @@ impl_writeable_tlv_based_enum!(OffersContext, }, ); +/// Contains a simple nonce for use in a blinded path's context. +/// +/// Such a context is required when receiving a [`DNSSECProof`] message. +/// +/// [`DNSSECProof`]: crate::onion_message::dns_resolution::DNSSECProof +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct DNSResolverContext { + /// A nonce which uniquely describes a DNS resolution. + /// + /// When we receive a DNSSEC proof message, we should check that it was sent over the blinded + /// path we included in the request by comparing a stored nonce with this one. + pub nonce: [u8; 16], +} + +impl_writeable_tlv_based!(DNSResolverContext, { + (0, nonce, required), +}); + /// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 5e1419fd0dd..8d69b09806d 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -18,7 +18,7 @@ use bitcoin::constants::ChainHash; use bitcoin::secp256k1::{self, Secp256k1, SecretKey, PublicKey}; -use crate::blinded_path::message::OffersContext; +use crate::blinded_path::message::{DNSResolverContext, OffersContext}; use crate::sign::{NodeSigner, Recipient}; use crate::events::{MessageSendEvent, MessageSendEventsProvider}; use crate::ln::types::ChannelId; @@ -161,7 +161,7 @@ impl DNSResolverMessageHandler for IgnoringMessageHandler { ) -> Option<(DNSResolverMessage, ResponseInstruction)> { None } - fn dnssec_proof(&self, _message: DNSSECProof) {} + fn dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} } impl CustomOnionMessageHandler for IgnoringMessageHandler { type CustomMessage = Infallible; diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 2e3bd36b08b..fd6651cb30e 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -17,6 +17,7 @@ use dnssec_prover::rr::Name; +use crate::blinded_path::message::DNSResolverContext; use crate::io; use crate::ln::msgs::DecodeError; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; @@ -39,7 +40,7 @@ pub trait DNSResolverMessageHandler { /// Handle a [`DNSSECProof`] message (in response to a [`DNSSECQuery`] we presumably sent). /// /// With this, we should be able to validate the DNS record we requested. - fn dnssec_proof(&self, message: DNSSECProof); + fn dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext); /// Release any [`DNSResolverMessage`]s that need to be sent. fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 7639f003b86..40c069c5765 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -10,7 +10,7 @@ //! Onion message testing and test utilities live here. use crate::blinded_path::EmptyNodeIdLookUp; -use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode, MessageContext, OffersContext}; +use crate::blinded_path::message::{BlindedMessagePath, DNSResolverContext, MessageForwardNode, MessageContext, OffersContext}; use crate::events::{Event, EventsProvider}; use crate::ln::features::{ChannelFeatures, InitFeatures}; use crate::ln::msgs::{self, DecodeError, OnionMessageHandler}; @@ -100,7 +100,7 @@ impl DNSResolverMessageHandler for TestDNSResolverMessageHandler { ) -> Option<(DNSResolverMessage, ResponseInstruction)> { None } - fn dnssec_proof(&self, _message: DNSSECProof) {} + fn dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} } #[derive(Clone, Debug, PartialEq)] diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 10001bdde29..bab2768df0f 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -994,6 +994,9 @@ where (ParsedOnionMessageContents::Custom(_), Some(MessageContext::Custom(_))) => { Ok(PeeledOnion::Receive(message, context, reply_path)) } + (ParsedOnionMessageContents::DNSResolver(_), Some(MessageContext::DNSResolver(_))) => { + Ok(PeeledOnion::Receive(message, context, reply_path)) + } _ => { log_trace!(logger, "Received message was sent on a blinded path with the wrong context."); Err(()) @@ -1604,7 +1607,7 @@ where let context = match context { None => None, Some(MessageContext::Offers(context)) => Some(context), - Some(MessageContext::Custom(_)) => { + Some(_) => { debug_assert!(false, "Shouldn't have triggered this case."); return } @@ -1634,13 +1637,17 @@ where } }, ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECProof(msg)) => { - self.dns_resolver_handler.dnssec_proof(msg); + let context = match context { + Some(MessageContext::DNSResolver(context)) => context, + _ => return, + }; + self.dns_resolver_handler.dnssec_proof(msg, context); }, ParsedOnionMessageContents::Custom(msg) => { let context = match context { None => None, Some(MessageContext::Custom(data)) => Some(data), - Some(MessageContext::Offers(_)) => { + Some(_) => { debug_assert!(false, "Shouldn't have triggered this case."); return } From f49d571ae5865397fd0210322486804ee56d6185 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 16:23:47 +0000 Subject: [PATCH 06/11] Add a type to track `HumanReadableName`s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BIP 353 `HumanReadableName`s are represented as `₿user@domain` and can be resolved using DNS into a `bitcoin:` URI. In the next commit, we will add such a resolver using onion messages to fetch records from the DNS, which will rely on this new type to get name information from outside LDK. --- lightning/src/onion_message/dns_resolution.rs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index fd6651cb30e..3f3c012a55b 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -141,3 +141,94 @@ impl OnionMessageContents for DNSResolverMessage { } } } + +/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts. +/// +/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be +/// non-empty. +/// +/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain +/// ASCII. +/// +/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct HumanReadableName { + // TODO Remove the heap allocations given the whole data can't be more than 256 bytes. + user: String, + domain: String, +} + +impl HumanReadableName { + /// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the + /// struct-level documentation for more on the requirements on each. + pub fn new(user: String, domain: String) -> Result { + const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1; + if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 { + return Err(()); + } + if user.is_empty() || domain.is_empty() { + return Err(()); + } + if !user.is_ascii() || !domain.is_ascii() { + return Err(()); + } + Ok(HumanReadableName { user, domain }) + } + + /// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`. + /// + /// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by + /// BIP 353. + pub fn from_encoded(encoded: &str) -> Result { + if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@") + { + Self::new(user.to_string(), domain.to_string()) + } else { + Err(()) + } + } + + /// Gets the `user` part of this Human Readable Name + pub fn user(&self) -> &str { + &self.user + } + + /// Gets the `domain` part of this Human Readable Name + pub fn domain(&self) -> &str { + &self.domain + } +} + +// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request` +impl Writeable for HumanReadableName { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + (self.user.len() as u8).write(writer)?; + writer.write_all(&self.user.as_bytes())?; + (self.domain.len() as u8).write(writer)?; + writer.write_all(&self.domain.as_bytes()) + } +} + +impl Readable for HumanReadableName { + fn read(reader: &mut R) -> Result { + let mut read_bytes = [0; 255]; + + let user_len: u8 = Readable::read(reader)?; + reader.read_exact(&mut read_bytes[..user_len as usize])?; + let user_bytes: Vec = read_bytes[..user_len as usize].into(); + let user = match String::from_utf8(user_bytes) { + Ok(user) => user, + Err(_) => return Err(DecodeError::InvalidValue), + }; + + let domain_len: u8 = Readable::read(reader)?; + reader.read_exact(&mut read_bytes[..domain_len as usize])?; + let domain_bytes: Vec = read_bytes[..domain_len as usize].into(); + let domain = match String::from_utf8(domain_bytes) { + Ok(domain) => domain, + Err(_) => return Err(DecodeError::InvalidValue), + }; + + HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue) + } +} From 3a6440a22cdce71cb3590af840543e8f8672a7c2 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:47:45 +0000 Subject: [PATCH 07/11] f use Hostname's validation --- lightning/src/onion_message/dns_resolution.rs | 2 +- lightning/src/util/ser.rs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 3f3c012a55b..78d1c6d5345 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -169,7 +169,7 @@ impl HumanReadableName { if user.is_empty() || domain.is_empty() { return Err(()); } - if !user.is_ascii() || !domain.is_ascii() { + if !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) { return Err(()); } Ok(HumanReadableName { user, domain }) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index a032291c4ae..16dd4accafe 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1490,6 +1490,16 @@ impl Hostname { pub fn len(&self) -> u8 { (&self.0).len() as u8 } + + /// Check if the chars in `s` are allowed to be included in a [`Hostname`]. + pub(crate) fn str_is_valid_hostname(s: &str) -> bool { + s.len() <= 255 && + s.chars().all(|c| + c.is_ascii_alphanumeric() || + c == '.' || + c == '-' + ) + } } impl core::fmt::Display for Hostname { @@ -1525,11 +1535,7 @@ impl TryFrom for Hostname { type Error = (); fn try_from(s: String) -> Result { - if s.len() <= 255 && s.chars().all(|c| - c.is_ascii_alphanumeric() || - c == '.' || - c == '-' - ) { + if Hostname::str_is_valid_hostname(&s) { Ok(Hostname(s)) } else { Err(()) From 72a01c9652cc0b95f23f6bc1e2e5377a3a35e037 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:57:42 +0000 Subject: [PATCH 08/11] Add the core functionality required to resolve Human Readable Names This adds a new utility struct, `OMNameResolver`, which implements the core functionality required to resolve Human Readable Names, namely generating `DNSSECQuery` onion messages, tracking the state of requests, and ultimately receiving and verifying `DNSSECProof` onion messages. It tracks pending requests with a `PaymentId`, allowing for easy integration into `ChannelManager` in a coming commit - mapping received proofs to `PaymentId`s which we can then complete by handing them `Offer`s to pay. It does not, directly, implement `DNSResolverMessageHandler`, but an implementation of `DNSResolverMessageHandler` becomes trivial with `OMNameResolver` handling the inbound messages and creating the messages to send. --- ci/ci-tests.sh | 6 + lightning/Cargo.toml | 2 + lightning/src/onion_message/dns_resolution.rs | 210 ++++++++++++++++++ 3 files changed, 218 insertions(+) diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index aff1a46c49e..6c60518803b 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -31,6 +31,12 @@ echo -e "\n\nBuilding and testing all workspace crates..." cargo test --verbose --color always cargo check --verbose --color always +echo -e "\n\nBuilding and testing lightning crate with dnssec feature" +pushd lightning +cargo test -p lightning --verbose --color always --features dnssec +cargo check -p lightning --verbose --color always --features dnssec +popd + echo -e "\n\nBuilding and testing Block Sync Clients with features" cargo test -p lightning-block-sync --verbose --color always --features rest-client diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 478b686e893..b26fa651ded 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -32,6 +32,8 @@ unsafe_revoked_tx_signing = [] no-std = ["hashbrown", "possiblyrandom", "libm"] std = [] +dnssec = ["dnssec-prover/validation"] + # Generates low-r bitcoin signatures, which saves 1 byte in 50% of the cases grind_signatures = [] diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 78d1c6d5345..fca99926252 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -12,17 +12,40 @@ //! It contains [`DNSResolverMessage`]s as well as a [`DNSResolverMessageHandler`] trait to handle //! such messages using an [`OnionMessenger`]. //! +//! With the `dnssec` feature enabled, it also contains `OMNameResolver`, which does all the work +//! required to resolve BIP 353 [`HumanReadableName`]s using [bLIP 32] - sending onion messages to +//! a DNS resolver, validating the proofs, and ultimately surfacing validated data back to the +//! caller. +//! //! [bLIP 32]: https://github.com/lightning/blips/blob/master/blip-0032.md //! [`OnionMessenger`]: super::messenger::OnionMessenger +#[cfg(feature = "dnssec")] +use core::str::FromStr; +#[cfg(feature = "dnssec")] +use core::sync::atomic::{AtomicUsize, Ordering}; + +#[cfg(feature = "dnssec")] +use dnssec_prover::rr::RR; +#[cfg(feature = "dnssec")] +use dnssec_prover::ser::parse_rr_stream; +#[cfg(feature = "dnssec")] +use dnssec_prover::validation::verify_rr_stream; + use dnssec_prover::rr::Name; use crate::blinded_path::message::DNSResolverContext; use crate::io; +#[cfg(feature = "dnssec")] +use crate::ln::channelmanager::PaymentId; use crate::ln::msgs::DecodeError; +#[cfg(feature = "dnssec")] +use crate::offers::offer::Offer; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; +#[cfg(feature = "dnssec")] +use crate::sync::Mutex; use crate::util::ser::{Hostname, Readable, ReadableArgs, Writeable, Writer}; /// A handler for an [`OnionMessage`] containing a DNS(SEC) query or a DNSSEC proof @@ -232,3 +255,190 @@ impl Readable for HumanReadableName { HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue) } } + +/// A stateful resolver which maps BIP 353 Human Readable Names to URIs and BOLT12 [`Offer`]s. +/// +/// It does not directly implement [`DNSResolverMessageHandler`] but implements all the core logic +/// which is required in a client which intends to. +/// +/// It relies on being made aware of the passage of time with regular calls to +/// [`Self::new_best_block`] in order to time out existing queries. Queries time out after two +/// blocks. +#[cfg(feature = "dnssec")] +pub struct OMNameResolver { + pending_resolves: + Mutex>>, + latest_block_time: AtomicUsize, + latest_block_height: AtomicUsize, +} + +#[cfg(feature = "dnssec")] +impl OMNameResolver { + /// Builds a new [`OMNameResolver`]. + pub fn new(latest_block_time: u32, latest_block_height: u32) -> Self { + Self { + pending_resolves: Mutex::new(new_hash_map()), + latest_block_time: AtomicUsize::new(latest_block_time as usize), + latest_block_height: AtomicUsize::new(latest_block_height as usize), + } + } + + /// Informs the [`OMNameResolver`] of the passage of time in the form of a new best Bitcoin + /// block. + /// + /// This will call back to resolve some pending queries which have timed out. + pub fn new_best_block(&self, height: u32, time: u32) { + self.latest_block_time.store(time as usize, Ordering::Release); + self.latest_block_height.store(height as usize, Ordering::Release); + let mut resolves = self.pending_resolves.lock().unwrap(); + resolves.retain(|_, queries| { + queries.retain_mut( + |(res_height, _, _, _)| { + if *res_height < height - 1 { + false + } else { + true + } + }, + ); + !queries.is_empty() + }); + } + + /// Begins the process of resolving a BIP 353 Human Readable Name. + /// + /// The given `random_context` must be a [`DNSResolverContext`] with a fresh, unused random + /// nonce which is included in the blinded path which will be set as the reply path when + /// sending the returned [`DNSSECQuery`]. + /// + /// Returns a [`DNSSECQuery`] onion message which should be sent to a resolver on success. + pub fn resolve_name( + &self, payment_id: PaymentId, name: HumanReadableName, random_context: DNSResolverContext, + ) -> Result { + let dns_name = + Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user, name.domain)); + debug_assert!( + dns_name.is_ok(), + "The HumanReadableName constructor shouldn't allow names which are too long" + ); + let name_query = dns_name.clone().map(|q| DNSSECQuery(q)); + if let Ok(dns_name) = dns_name { + let height = self.latest_block_height.load(Ordering::Acquire); + let mut pending_resolves = self.pending_resolves.lock().unwrap(); + let resolution = (height as u32, random_context, name, payment_id); + pending_resolves.entry(dns_name).or_insert_with(Vec::new).push(resolution); + } + name_query + } + + /// Handles a [`DNSSECProof`] message, attempting to verify it and match it against a pending + /// query. + /// + /// If verification succeeds, the resulting bitcoin: URI is parsed to find a contained + /// [`Offer`]. + /// + /// Note that a single proof for a wildcard DNS entry may complete several requests for + /// different [`HumanReadableName`]s. + /// + /// If an [`Offer`] is found, it, as well as the [`PaymentId`] and original `name` passed to + /// [`Self::resolve_name`] are returned. + pub fn handle_dnssec_proof_for_offer( + &self, msg: DNSSECProof, context: DNSResolverContext, + ) -> Option<(Vec<(HumanReadableName, PaymentId)>, Offer)> { + let (completed_requests, uri) = self.handle_dnssec_proof_for_uri(msg, context)?; + if let Some((_onchain, params)) = uri.split_once("?") { + for param in params.split("&") { + let (k, v) = if let Some(split) = param.split_once("=") { + split + } else { + continue; + }; + if k.eq_ignore_ascii_case("lno") { + if let Ok(offer) = Offer::from_str(v) { + return Some((completed_requests, offer)); + } + return None; + } + } + } + None + } + + /// Handles a [`DNSSECProof`] message, attempting to verify it and match it against any pending + /// queries. + /// + /// If verification succeeds, all matching [`PaymentId`] and [`HumanReadableName`]s passed to + /// [`Self::resolve_name`], as well as the resolved bitcoin: URI are returned. + /// + /// Note that a single proof for a wildcard DNS entry may complete several requests for + /// different [`HumanReadableName`]s. + /// + /// This method is useful for those who handle bitcoin: URIs already, handling more than just + /// BOLT12 [`Offer`]s. + pub fn handle_dnssec_proof_for_uri( + &self, msg: DNSSECProof, context: DNSResolverContext, + ) -> Option<(Vec<(HumanReadableName, PaymentId)>, String)> { + let DNSSECProof { name: answer_name, proof } = msg; + let mut pending_resolves = self.pending_resolves.lock().unwrap(); + if let hash_map::Entry::Occupied(entry) = pending_resolves.entry(answer_name) { + if !entry.get().iter().any(|query| query.1 == context) { + // If we don't have any pending queries with the context included in the blinded + // path (implying someone sent us this response not using the blinded path we gave + // when making the query), return immediately to avoid the extra time for the proof + // validation giving away that we were the node that made the query. + // + // If there was at least one query with the same context, we go ahead and complete + // all queries for the same name, as there's no point in waiting for another proof + // for the same name. + return None; + } + let parsed_rrs = parse_rr_stream(&proof); + let validated_rrs = + parsed_rrs.as_ref().and_then(|rrs| verify_rr_stream(rrs).map_err(|_| &())); + if let Ok(validated_rrs) = validated_rrs { + let block_time = self.latest_block_time.load(Ordering::Acquire) as u64; + // Block times may be up to two hours in the future and some time into the past + // (we assume no more than two hours, though the actual limits are rather + // complicated). + // Thus, we have to let the proof times be rather fuzzy. + if validated_rrs.valid_from > block_time + 60 * 2 { + return None; + } + if validated_rrs.expires < block_time - 60 * 2 { + return None; + } + let resolved_rrs = validated_rrs.resolve_name(&entry.key()); + if resolved_rrs.is_empty() { + return None; + } + + let (_, requests) = entry.remove_entry(); + + const URI_PREFIX: &str = "bitcoin:"; + let mut candidate_records = resolved_rrs + .iter() + .filter_map( + |rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None }, + ) + .filter_map( + |data| if let Ok(s) = String::from_utf8(data) { Some(s) } else { None }, + ) + .filter(|data_string| data_string.len() > URI_PREFIX.len()) + .filter(|data_string| { + data_string[..URI_PREFIX.len()].eq_ignore_ascii_case(URI_PREFIX) + }); + // Check that there is exactly one TXT record that begins with + // bitcoin: as required by BIP 353 (and is valid UTF-8). + match (candidate_records.next(), candidate_records.next()) { + (Some(txt), None) => { + let completed_requests = + requests.into_iter().map(|(_, _, id, name)| (id, name)).collect(); + return Some((completed_requests, txt)); + }, + _ => {}, + } + } + } + None + } +} From 7018be4bc1f11cabd39fd77e294e9f115efe225e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:57:53 +0000 Subject: [PATCH 09/11] f use struct for queries --- lightning/src/onion_message/dns_resolution.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index fca99926252..7d32de383cc 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -256,6 +256,14 @@ impl Readable for HumanReadableName { } } +#[cfg(feature = "dnssec")] +struct PendingResolution { + start_height: u32, + context: DNSResolverContext, + name: HumanReadableName, + payment_id: PaymentId, +} + /// A stateful resolver which maps BIP 353 Human Readable Names to URIs and BOLT12 [`Offer`]s. /// /// It does not directly implement [`DNSResolverMessageHandler`] but implements all the core logic @@ -266,8 +274,7 @@ impl Readable for HumanReadableName { /// blocks. #[cfg(feature = "dnssec")] pub struct OMNameResolver { - pending_resolves: - Mutex>>, + pending_resolves: Mutex>>, latest_block_time: AtomicUsize, latest_block_height: AtomicUsize, } @@ -292,15 +299,7 @@ impl OMNameResolver { self.latest_block_height.store(height as usize, Ordering::Release); let mut resolves = self.pending_resolves.lock().unwrap(); resolves.retain(|_, queries| { - queries.retain_mut( - |(res_height, _, _, _)| { - if *res_height < height - 1 { - false - } else { - true - } - }, - ); + queries.retain(|query| query.start_height >= height - 1); !queries.is_empty() }); } @@ -323,9 +322,10 @@ impl OMNameResolver { ); let name_query = dns_name.clone().map(|q| DNSSECQuery(q)); if let Ok(dns_name) = dns_name { - let height = self.latest_block_height.load(Ordering::Acquire); + let start_height = self.latest_block_height.load(Ordering::Acquire) as u32; let mut pending_resolves = self.pending_resolves.lock().unwrap(); - let resolution = (height as u32, random_context, name, payment_id); + let context = random_context; + let resolution = PendingResolution { start_height, context, name, payment_id }; pending_resolves.entry(dns_name).or_insert_with(Vec::new).push(resolution); } name_query @@ -381,7 +381,7 @@ impl OMNameResolver { let DNSSECProof { name: answer_name, proof } = msg; let mut pending_resolves = self.pending_resolves.lock().unwrap(); if let hash_map::Entry::Occupied(entry) = pending_resolves.entry(answer_name) { - if !entry.get().iter().any(|query| query.1 == context) { + if !entry.get().iter().any(|query| query.context == context) { // If we don't have any pending queries with the context included in the blinded // path (implying someone sent us this response not using the blinded path we gave // when making the query), return immediately to avoid the extra time for the proof @@ -432,7 +432,7 @@ impl OMNameResolver { match (candidate_records.next(), candidate_records.next()) { (Some(txt), None) => { let completed_requests = - requests.into_iter().map(|(_, _, id, name)| (id, name)).collect(); + requests.into_iter().map(|r| (r.name, r.payment_id)).collect(); return Some((completed_requests, txt)); }, _ => {}, From eaa1e2ee2c6ef278ef029d64955307455e468d95 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Sep 2024 17:28:32 +0000 Subject: [PATCH 10/11] f take an entropysource to force rng use --- lightning/src/onion_message/dns_resolution.rs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 7d32de383cc..2c9c285c433 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -45,6 +45,8 @@ use crate::onion_message::messenger::{MessageSendInstructions, Responder, Respon use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; #[cfg(feature = "dnssec")] +use crate::sign::EntropySource; +#[cfg(feature = "dnssec")] use crate::sync::Mutex; use crate::util::ser::{Hostname, Readable, ReadableArgs, Writeable, Writer}; @@ -306,29 +308,29 @@ impl OMNameResolver { /// Begins the process of resolving a BIP 353 Human Readable Name. /// - /// The given `random_context` must be a [`DNSResolverContext`] with a fresh, unused random - /// nonce which is included in the blinded path which will be set as the reply path when - /// sending the returned [`DNSSECQuery`]. - /// - /// Returns a [`DNSSECQuery`] onion message which should be sent to a resolver on success. - pub fn resolve_name( - &self, payment_id: PaymentId, name: HumanReadableName, random_context: DNSResolverContext, - ) -> Result { + /// Returns a [`DNSSECQuery`] onion message and a [`DNSResolverContext`] which should be sent + /// to a resolver (with the context used to generate the blinded response path) on success. + pub fn resolve_name( + &self, payment_id: PaymentId, name: HumanReadableName, entropy_source: &ES, + ) -> Result<(DNSSECQuery, DNSResolverContext), ()> { let dns_name = Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user, name.domain)); debug_assert!( dns_name.is_ok(), "The HumanReadableName constructor shouldn't allow names which are too long" ); - let name_query = dns_name.clone().map(|q| DNSSECQuery(q)); + let mut context = DNSResolverContext { nonce: [0; 16] }; + context.nonce.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]); if let Ok(dns_name) = dns_name { let start_height = self.latest_block_height.load(Ordering::Acquire) as u32; let mut pending_resolves = self.pending_resolves.lock().unwrap(); - let context = random_context; + let context_ret = context.clone(); let resolution = PendingResolution { start_height, context, name, payment_id }; - pending_resolves.entry(dns_name).or_insert_with(Vec::new).push(resolution); + pending_resolves.entry(dns_name.clone()).or_insert_with(Vec::new).push(resolution); + Ok((DNSSECQuery(dns_name), context_ret)) + } else { + Err(()) } - name_query } /// Handles a [`DNSSECProof`] message, attempting to verify it and match it against a pending From 03406f7b9ac9388e5a2b0a93765c712f6e86b91c Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:31:49 +0000 Subject: [PATCH 11/11] f add dnssec feature to docsrs --- lightning/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index b26fa651ded..0197da58f5d 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -12,7 +12,7 @@ Still missing tons of error-handling. See GitHub issues for suggested projects i edition = "2021" [package.metadata.docs.rs] -features = ["std"] +features = ["std", "dnssec"] rustdoc-args = ["--cfg", "docsrs"] [features]