From 151a8a1aaf828195a5198b64a97a08c645dc9b60 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 30 Sep 2024 14:45:36 +0000 Subject: [PATCH 1/6] 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 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 805d35a010c..5b6b5fa6dbd 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -284,6 +284,9 @@ pub enum MessageContext { /// /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage AsyncPayments(AsyncPaymentsContext), + /// Represents a context for a blinded path used in a reply path when requesting a DNSSEC proof + /// in a `DNSResolverMessage`. + DNSResolver(DNSResolverContext), /// Context specific to a [`CustomOnionMessageHandler::CustomMessage`]. /// /// [`CustomOnionMessageHandler::CustomMessage`]: crate::onion_message::messenger::CustomOnionMessageHandler::CustomMessage @@ -402,6 +405,7 @@ impl_writeable_tlv_based_enum!(MessageContext, {0, Offers} => (), {1, Custom} => (), {2, AsyncPayments} => (), + {3, DNSResolver} => (), ); impl_writeable_tlv_based_enum!(OffersContext, @@ -428,6 +432,22 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, }, ); +/// Contains a simple nonce for use in a blinded path's context. +/// +/// Such a context is required when receiving a `DNSSECProof` message. +#[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], From 1cf03930561cd49017cbff8237ef45cdab379fb0 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 30 Sep 2024 16:16:36 +0000 Subject: [PATCH 2/6] 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/blinded_path/message.rs | 8 +- lightning/src/onion_message/dns_resolution.rs | 146 ++++++++++++++++++ lightning/src/onion_message/mod.rs | 1 + lightning/src/util/ser.rs | 10 ++ 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 lightning/src/onion_message/dns_resolution.rs diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 3a1939733a6..e0eb5043146 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -43,8 +43,10 @@ lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default 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", default-features = false } possiblyrandom = { version = "0.2", path = "../possiblyrandom", default-features = false } + regex = { version = "1.5.6", optional = true } backtrace = { version = "0.3", optional = true } diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 5b6b5fa6dbd..2cad33887f4 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -285,7 +285,9 @@ pub enum MessageContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage AsyncPayments(AsyncPaymentsContext), /// Represents a context for a blinded path used in a reply path when requesting a DNSSEC proof - /// in a `DNSResolverMessage`. + /// in a [`DNSResolverMessage`]. + /// + /// [`DNSResolverMessage`]: crate::onion_message::dns_resolution::DNSResolverMessage DNSResolver(DNSResolverContext), /// Context specific to a [`CustomOnionMessageHandler::CustomMessage`]. /// @@ -434,7 +436,9 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, /// Contains a simple nonce for use in a blinded path's context. /// -/// Such a context is required when receiving a `DNSSECProof` message. +/// 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. diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs new file mode 100644 index 00000000000..3d531264a82 --- /dev/null +++ b/lightning/src/onion_message/dns_resolution.rs @@ -0,0 +1,146 @@ +// 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::blinded_path::message::DNSResolverContext; +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::{Hostname, 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 handle_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 handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext); + + /// 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 receive +/// DNSSEC proofs. +pub enum DNSResolverMessage { + /// A query requesting a DNSSEC proof + DNSSECQuery(DNSSECQuery), + /// A response containing a DNSSEC proof + DNSSECProof(DNSSECProof), +} + +const DNSSEC_QUERY_TYPE: u64 = 65536; +const DNSSEC_PROOF_TYPE: u64 = 65538; + +#[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); + +#[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, + /// An [RFC 9102 DNSSEC AuthenticationChain] providing a DNSSEC proof. + /// + /// [RFC 9102 DNSSEC AuthenticationChain]: https://www.rfc-editor.org/rfc/rfc9102.html#name-dnssec-authentication-chain + pub proof: Vec, +} + +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) + }, + } + } +} + +impl ReadableArgs for DNSResolverMessage { + fn read(r: &mut R, message_type: u64) -> Result { + match message_type { + DNSSEC_QUERY_TYPE => { + let s = Hostname::read(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + Ok(DNSResolverMessage::DNSSECQuery(DNSSECQuery(name))) + }, + DNSSEC_PROOF_TYPE => { + 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 })) + }, + _ => 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; diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 99d20b927b6..b847f9eebd7 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 ebde296abd0c1e1b15d7e2ece9c4f7f961efb378 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 17:08:27 +0000 Subject: [PATCH 3/6] 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 | 11 ++- .../src/onion_message/functional_tests.rs | 22 +++++- lightning/src/onion_message/messenger.rs | 78 ++++++++++++++----- lightning/src/onion_message/packet.rs | 18 ++++- 8 files changed, 114 insertions(+), 35 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 5cd45238df2..cf87467a5ea 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -11,6 +11,7 @@ use lightning::blinded_path::message::{ 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; @@ -56,6 +57,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 fdeb46229a3..318dfcbbd7e 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -657,7 +657,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>; /// # @@ -1202,6 +1202,7 @@ mod tests { IgnoringMessageHandler, Arc, IgnoringMessageHandler, + IgnoringMessageHandler, >; struct Node { @@ -1604,6 +1605,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 afab38c7bc2..7776966b285 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -415,6 +415,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, >; @@ -3283,6 +3284,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), @@ -231,9 +229,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), @@ -252,9 +248,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 cb61b4dee9e..34b3f1ba9bd 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::{AsyncPaymentsContext, OffersContext}; +use crate::blinded_path::message::{AsyncPaymentsContext, DNSResolverContext, OffersContext}; use crate::sign::{NodeSigner, Recipient}; use crate::events::{MessageSendEvent, MessageSendEventsProvider}; use crate::ln::types::ChannelId; @@ -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, _context: AsyncPaymentsContext) {} } +impl DNSResolverMessageHandler for IgnoringMessageHandler { + fn handle_dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} +} 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 ef733b0893c..ac79c88fb00 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::{AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, MessageContext, OffersContext}; +use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageForwardNode, MessageContext, OffersContext}; use crate::events::{Event, EventsProvider}; use crate::ln::features::{ChannelFeatures, InitFeatures}; use crate::ln::msgs::{self, DecodeError, OnionMessageHandler}; @@ -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, _context: AsyncPaymentsContext) {} } +struct TestDNSResolverMessageHandler {} + +impl DNSResolverMessageHandler for TestDNSResolverMessageHandler { + fn handle_dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} +} + #[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 4b7ba02e4dd..c531846912f 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>, @@ -986,6 +998,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(()) @@ -1071,8 +1086,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, @@ -1081,17 +1096,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, ) } @@ -1118,18 +1134,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()); @@ -1143,6 +1160,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()), @@ -1479,8 +1497,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, @@ -1489,6 +1507,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 { @@ -1564,8 +1583,8 @@ where } } -impl OnionMessageHandler -for OnionMessenger +impl OnionMessageHandler +for OnionMessenger where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1574,6 +1593,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) { @@ -1622,6 +1642,19 @@ where }; self.async_payments_handler.release_held_htlc(msg, context); }, + ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECQuery(msg)) => { + let response_instructions = self.dns_resolver_handler.handle_dnssec_query(msg, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECProof(msg)) => { + let context = match context { + Some(MessageContext::DNSResolver(context)) => context, + _ => return, + }; + self.dns_resolver_handler.handle_dnssec_proof(msg, context); + }, ParsedOnionMessageContents::Custom(msg) => { let context = match context { None => None, @@ -1773,6 +1806,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( @@ -1804,6 +1844,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>>, Arc, Arc>>, Arc>, Arc>, + IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) IgnoringMessageHandler >; @@ -1825,6 +1866,7 @@ pub type SimpleRefOnionMessenger< &'i DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 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 75d20e5a5c23eac3ce0850cc9969a6b7feeae807 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 16:23:47 +0000 Subject: [PATCH 4/6] 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 +++++++++++++++++++ lightning/src/util/ser.rs | 16 +++- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 3d531264a82..489b2ed0673 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -144,3 +144,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 !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) { + 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) + } +} diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index b847f9eebd7..21ce044e3c9 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 829dc34d103ef921f2346fb7e9dc1aa8fb9212e9 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 30 Sep 2024 18:03:12 +0000 Subject: [PATCH 5/6] Allow `_` in `Hostname`s These are perfectly fine and are relied on by BIP 353, so we need to ensure we allow them. --- lightning/src/util/ser.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 21ce044e3c9..e27d492930e 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1496,8 +1496,7 @@ impl Hostname { s.len() <= 255 && s.chars().all(|c| c.is_ascii_alphanumeric() || - c == '.' || - c == '-' + c == '.' || c == '_' || c == '-' ) } } From 9335c9bbf799eaadaef732cfed9c91121af7e228 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 12 Sep 2024 15:57:42 +0000 Subject: [PATCH 6/6] 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 | 4 + lightning/Cargo.toml | 4 +- lightning/src/onion_message/dns_resolution.rs | 212 ++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 47f0621683c..ed2efbaad2d 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -56,6 +56,10 @@ for DIR in "${WORKSPACE_MEMBERS[@]}"; do cargo doc -p "$DIR" --document-private-items done +echo -e "\n\nChecking and testing lightning crate with dnssec feature" +cargo test -p lightning --verbose --color always --features dnssec +cargo check -p lightning --verbose --color always --features dnssec + echo -e "\n\nChecking 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 e0eb5043146..834e064e815 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] @@ -31,6 +31,8 @@ unsafe_revoked_tx_signing = [] 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 489b2ed0673..af133aa7ab4 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -12,17 +12,42 @@ //! 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::sign::EntropySource; +#[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 @@ -235,3 +260,190 @@ impl Readable for HumanReadableName { HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue) } } + +#[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 +/// 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(|query| query.start_height >= height - 1); + !queries.is_empty() + }); + } + + /// Begins the process of resolving a BIP 353 Human Readable Name. + /// + /// 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 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_ret = context.clone(); + let resolution = PendingResolution { start_height, context, name, payment_id }; + pending_resolves.entry(dns_name.clone()).or_insert_with(Vec::new).push(resolution); + Ok((DNSSECQuery(dns_name), context_ret)) + } else { + Err(()) + } + } + + /// 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.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 + // 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(|r| (r.name, r.payment_id)).collect(); + return Some((completed_requests, txt)); + }, + _ => {}, + } + } + } + None + } +}