lightning/onion_message/
dns_resolution.rs

1// This file is Copyright its original authors, visible in version control
2// history.
3//
4// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7// You may not use this file except in accordance with one or both of these
8// licenses.
9
10//! This module defines message handling for DNSSEC proof fetching using [bLIP 32].
11//!
12//! It contains [`DNSResolverMessage`]s as well as a [`DNSResolverMessageHandler`] trait to handle
13//! such messages using an [`OnionMessenger`].
14//!
15//! With the `dnssec` feature enabled, it also contains `OMNameResolver`, which does all the work
16//! required to resolve BIP 353 [`HumanReadableName`]s using [bLIP 32] - sending onion messages to
17//! a DNS resolver, validating the proofs, and ultimately surfacing validated data back to the
18//! caller.
19//!
20//! [bLIP 32]: https://github.com/lightning/blips/blob/master/blip-0032.md
21//! [`OnionMessenger`]: super::messenger::OnionMessenger
22
23#[cfg(feature = "dnssec")]
24use core::str::FromStr;
25#[cfg(feature = "dnssec")]
26use core::sync::atomic::{AtomicUsize, Ordering};
27
28#[cfg(feature = "dnssec")]
29use dnssec_prover::rr::RR;
30#[cfg(feature = "dnssec")]
31use dnssec_prover::ser::parse_rr_stream;
32#[cfg(feature = "dnssec")]
33use dnssec_prover::validation::verify_rr_stream;
34
35use dnssec_prover::rr::Name;
36
37use lightning_types::features::NodeFeatures;
38
39use crate::blinded_path::message::DNSResolverContext;
40use crate::io;
41#[cfg(feature = "dnssec")]
42use crate::ln::channelmanager::PaymentId;
43use crate::ln::msgs::DecodeError;
44#[cfg(feature = "dnssec")]
45use crate::offers::offer::Offer;
46use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction};
47use crate::onion_message::packet::OnionMessageContents;
48use crate::prelude::*;
49#[cfg(feature = "dnssec")]
50use crate::sign::EntropySource;
51#[cfg(feature = "dnssec")]
52use crate::sync::Mutex;
53use crate::util::ser::{Hostname, Readable, ReadableArgs, Writeable, Writer};
54
55/// A handler for an [`OnionMessage`] containing a DNS(SEC) query or a DNSSEC proof
56///
57/// [`OnionMessage`]: crate::ln::msgs::OnionMessage
58pub trait DNSResolverMessageHandler {
59	/// Handle a [`DNSSECQuery`] message.
60	///
61	/// If we provide DNS resolution services to third parties, we should respond with a
62	/// [`DNSSECProof`] message.
63	fn handle_dnssec_query(
64		&self, message: DNSSECQuery, responder: Option<Responder>,
65	) -> Option<(DNSResolverMessage, ResponseInstruction)>;
66
67	/// Handle a [`DNSSECProof`] message (in response to a [`DNSSECQuery`] we presumably sent).
68	///
69	/// With this, we should be able to validate the DNS record we requested.
70	fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext);
71
72	/// Gets the node feature flags which this handler itself supports. Useful for setting the
73	/// `dns_resolver` flag if this handler supports returning [`DNSSECProof`] messages in response
74	/// to [`DNSSECQuery`] messages.
75	fn provided_node_features(&self) -> NodeFeatures {
76		NodeFeatures::empty()
77	}
78
79	/// Release any [`DNSResolverMessage`]s that need to be sent.
80	fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> {
81		vec![]
82	}
83}
84
85#[derive(Clone, Debug, Hash, PartialEq, Eq)]
86/// An enum containing the possible onion messages which are used uses to request and receive
87/// DNSSEC proofs.
88pub enum DNSResolverMessage {
89	/// A query requesting a DNSSEC proof
90	DNSSECQuery(DNSSECQuery),
91	/// A response containing a DNSSEC proof
92	DNSSECProof(DNSSECProof),
93}
94
95const DNSSEC_QUERY_TYPE: u64 = 65536;
96const DNSSEC_PROOF_TYPE: u64 = 65538;
97
98#[derive(Clone, Debug, Hash, PartialEq, Eq)]
99/// A message which is sent to a DNSSEC prover requesting a DNSSEC proof for the given name.
100pub struct DNSSECQuery(pub Name);
101
102#[derive(Clone, Debug, Hash, PartialEq, Eq)]
103/// A message which is sent in response to [`DNSSECQuery`] containing a DNSSEC proof.
104pub struct DNSSECProof {
105	/// The name which the query was for. The proof may not contain a DNS RR for exactly this name
106	/// if it contains a wildcard RR which contains this name instead.
107	pub name: Name,
108	/// An [RFC 9102 DNSSEC AuthenticationChain] providing a DNSSEC proof.
109	///
110	/// [RFC 9102 DNSSEC AuthenticationChain]: https://www.rfc-editor.org/rfc/rfc9102.html#name-dnssec-authentication-chain
111	pub proof: Vec<u8>,
112}
113
114impl DNSResolverMessage {
115	/// Returns whether `tlv_type` corresponds to a TLV record for DNS Resolvers.
116	pub fn is_known_type(tlv_type: u64) -> bool {
117		match tlv_type {
118			DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE => true,
119			_ => false,
120		}
121	}
122}
123
124impl Writeable for DNSResolverMessage {
125	fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
126		match self {
127			Self::DNSSECQuery(DNSSECQuery(q)) => {
128				(q.as_str().len() as u8).write(w)?;
129				w.write_all(&q.as_str().as_bytes())
130			},
131			Self::DNSSECProof(DNSSECProof { name, proof }) => {
132				(name.as_str().len() as u8).write(w)?;
133				w.write_all(&name.as_str().as_bytes())?;
134				proof.write(w)
135			},
136		}
137	}
138}
139
140impl ReadableArgs<u64> for DNSResolverMessage {
141	fn read<R: io::Read>(r: &mut R, message_type: u64) -> Result<Self, DecodeError> {
142		match message_type {
143			DNSSEC_QUERY_TYPE => {
144				let s = Hostname::read(r)?;
145				let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?;
146				Ok(DNSResolverMessage::DNSSECQuery(DNSSECQuery(name)))
147			},
148			DNSSEC_PROOF_TYPE => {
149				let s = Hostname::read(r)?;
150				let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?;
151				let proof = Readable::read(r)?;
152				Ok(DNSResolverMessage::DNSSECProof(DNSSECProof { name, proof }))
153			},
154			_ => Err(DecodeError::InvalidValue),
155		}
156	}
157}
158
159impl OnionMessageContents for DNSResolverMessage {
160	#[cfg(c_bindings)]
161	fn msg_type(&self) -> String {
162		match self {
163			DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query".to_string(),
164			DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof".to_string(),
165		}
166	}
167	#[cfg(not(c_bindings))]
168	fn msg_type(&self) -> &'static str {
169		match self {
170			DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query",
171			DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof",
172		}
173	}
174	fn tlv_type(&self) -> u64 {
175		match self {
176			DNSResolverMessage::DNSSECQuery(_) => DNSSEC_QUERY_TYPE,
177			DNSResolverMessage::DNSSECProof(_) => DNSSEC_PROOF_TYPE,
178		}
179	}
180}
181
182/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts.
183///
184/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be
185/// non-empty.
186///
187/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain
188/// ASCII.
189///
190/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
191#[derive(Clone, Debug, Hash, PartialEq, Eq)]
192pub struct HumanReadableName {
193	// TODO Remove the heap allocations given the whole data can't be more than 256 bytes.
194	user: String,
195	domain: String,
196}
197
198impl HumanReadableName {
199	/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
200	/// struct-level documentation for more on the requirements on each.
201	pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
202		// First normalize domain and remove the optional trailing `.`
203		if domain.ends_with(".") {
204			domain.pop();
205		}
206		// Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.`
207		const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
208		if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
209			return Err(());
210		}
211		if user.is_empty() || domain.is_empty() {
212			return Err(());
213		}
214		if !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) {
215			return Err(());
216		}
217		Ok(HumanReadableName { user, domain })
218	}
219
220	/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
221	///
222	/// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by
223	/// BIP 353.
224	pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
225		if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
226		{
227			Self::new(user.to_string(), domain.to_string())
228		} else {
229			Err(())
230		}
231	}
232
233	/// Gets the `user` part of this Human Readable Name
234	pub fn user(&self) -> &str {
235		&self.user
236	}
237
238	/// Gets the `domain` part of this Human Readable Name
239	pub fn domain(&self) -> &str {
240		&self.domain
241	}
242}
243
244// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request`
245impl Writeable for HumanReadableName {
246	fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
247		(self.user.len() as u8).write(writer)?;
248		writer.write_all(&self.user.as_bytes())?;
249		(self.domain.len() as u8).write(writer)?;
250		writer.write_all(&self.domain.as_bytes())
251	}
252}
253
254impl Readable for HumanReadableName {
255	fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
256		let mut read_bytes = [0; 255];
257
258		let user_len: u8 = Readable::read(reader)?;
259		reader.read_exact(&mut read_bytes[..user_len as usize])?;
260		let user_bytes: Vec<u8> = read_bytes[..user_len as usize].into();
261		let user = match String::from_utf8(user_bytes) {
262			Ok(user) => user,
263			Err(_) => return Err(DecodeError::InvalidValue),
264		};
265
266		let domain_len: u8 = Readable::read(reader)?;
267		reader.read_exact(&mut read_bytes[..domain_len as usize])?;
268		let domain_bytes: Vec<u8> = read_bytes[..domain_len as usize].into();
269		let domain = match String::from_utf8(domain_bytes) {
270			Ok(domain) => domain,
271			Err(_) => return Err(DecodeError::InvalidValue),
272		};
273
274		HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue)
275	}
276}
277
278#[cfg(feature = "dnssec")]
279struct PendingResolution {
280	start_height: u32,
281	context: DNSResolverContext,
282	name: HumanReadableName,
283	payment_id: PaymentId,
284}
285
286/// A stateful resolver which maps BIP 353 Human Readable Names to URIs and BOLT12 [`Offer`]s.
287///
288/// It does not directly implement [`DNSResolverMessageHandler`] but implements all the core logic
289/// which is required in a client which intends to.
290///
291/// It relies on being made aware of the passage of time with regular calls to
292/// [`Self::new_best_block`] in order to time out existing queries. Queries time out after two
293/// blocks.
294#[cfg(feature = "dnssec")]
295pub struct OMNameResolver {
296	pending_resolves: Mutex<HashMap<Name, Vec<PendingResolution>>>,
297	latest_block_time: AtomicUsize,
298	latest_block_height: AtomicUsize,
299}
300
301#[cfg(feature = "dnssec")]
302impl OMNameResolver {
303	/// Builds a new [`OMNameResolver`].
304	pub fn new(latest_block_time: u32, latest_block_height: u32) -> Self {
305		Self {
306			pending_resolves: Mutex::new(new_hash_map()),
307			latest_block_time: AtomicUsize::new(latest_block_time as usize),
308			latest_block_height: AtomicUsize::new(latest_block_height as usize),
309		}
310	}
311
312	/// Informs the [`OMNameResolver`] of the passage of time in the form of a new best Bitcoin
313	/// block.
314	///
315	/// This will call back to resolve some pending queries which have timed out.
316	pub fn new_best_block(&self, height: u32, time: u32) {
317		self.latest_block_time.store(time as usize, Ordering::Release);
318		self.latest_block_height.store(height as usize, Ordering::Release);
319		let mut resolves = self.pending_resolves.lock().unwrap();
320		resolves.retain(|_, queries| {
321			queries.retain(|query| query.start_height >= height - 1);
322			!queries.is_empty()
323		});
324	}
325
326	/// Begins the process of resolving a BIP 353 Human Readable Name.
327	///
328	/// Returns a [`DNSSECQuery`] onion message and a [`DNSResolverContext`] which should be sent
329	/// to a resolver (with the context used to generate the blinded response path) on success.
330	pub fn resolve_name<ES: EntropySource + ?Sized>(
331		&self, payment_id: PaymentId, name: HumanReadableName, entropy_source: &ES,
332	) -> Result<(DNSSECQuery, DNSResolverContext), ()> {
333		let dns_name =
334			Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user, name.domain));
335		debug_assert!(
336			dns_name.is_ok(),
337			"The HumanReadableName constructor shouldn't allow names which are too long"
338		);
339		let mut context = DNSResolverContext { nonce: [0; 16] };
340		context.nonce.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]);
341		if let Ok(dns_name) = dns_name {
342			let start_height = self.latest_block_height.load(Ordering::Acquire) as u32;
343			let mut pending_resolves = self.pending_resolves.lock().unwrap();
344			let context_ret = context.clone();
345			let resolution = PendingResolution { start_height, context, name, payment_id };
346			pending_resolves.entry(dns_name.clone()).or_insert_with(Vec::new).push(resolution);
347			Ok((DNSSECQuery(dns_name), context_ret))
348		} else {
349			Err(())
350		}
351	}
352
353	/// Handles a [`DNSSECProof`] message, attempting to verify it and match it against a pending
354	/// query.
355	///
356	/// If verification succeeds, the resulting bitcoin: URI is parsed to find a contained
357	/// [`Offer`].
358	///
359	/// Note that a single proof for a wildcard DNS entry may complete several requests for
360	/// different [`HumanReadableName`]s.
361	///
362	/// If an [`Offer`] is found, it, as well as the [`PaymentId`] and original `name` passed to
363	/// [`Self::resolve_name`] are returned.
364	pub fn handle_dnssec_proof_for_offer(
365		&self, msg: DNSSECProof, context: DNSResolverContext,
366	) -> Option<(Vec<(HumanReadableName, PaymentId)>, Offer)> {
367		let (completed_requests, uri) = self.handle_dnssec_proof_for_uri(msg, context)?;
368		if let Some((_onchain, params)) = uri.split_once("?") {
369			for param in params.split("&") {
370				let (k, v) = if let Some(split) = param.split_once("=") {
371					split
372				} else {
373					continue;
374				};
375				if k.eq_ignore_ascii_case("lno") {
376					if let Ok(offer) = Offer::from_str(v) {
377						return Some((completed_requests, offer));
378					}
379					return None;
380				}
381			}
382		}
383		None
384	}
385
386	/// Handles a [`DNSSECProof`] message, attempting to verify it and match it against any pending
387	/// queries.
388	///
389	/// If verification succeeds, all matching [`PaymentId`] and [`HumanReadableName`]s passed to
390	/// [`Self::resolve_name`], as well as the resolved bitcoin: URI are returned.
391	///
392	/// Note that a single proof for a wildcard DNS entry may complete several requests for
393	/// different [`HumanReadableName`]s.
394	///
395	/// This method is useful for those who handle bitcoin: URIs already, handling more than just
396	/// BOLT12 [`Offer`]s.
397	pub fn handle_dnssec_proof_for_uri(
398		&self, msg: DNSSECProof, context: DNSResolverContext,
399	) -> Option<(Vec<(HumanReadableName, PaymentId)>, String)> {
400		let DNSSECProof { name: answer_name, proof } = msg;
401		let mut pending_resolves = self.pending_resolves.lock().unwrap();
402		if let hash_map::Entry::Occupied(entry) = pending_resolves.entry(answer_name) {
403			if !entry.get().iter().any(|query| query.context == context) {
404				// If we don't have any pending queries with the context included in the blinded
405				// path (implying someone sent us this response not using the blinded path we gave
406				// when making the query), return immediately to avoid the extra time for the proof
407				// validation giving away that we were the node that made the query.
408				//
409				// If there was at least one query with the same context, we go ahead and complete
410				// all queries for the same name, as there's no point in waiting for another proof
411				// for the same name.
412				return None;
413			}
414			let parsed_rrs = parse_rr_stream(&proof);
415			let validated_rrs =
416				parsed_rrs.as_ref().and_then(|rrs| verify_rr_stream(rrs).map_err(|_| &()));
417			if let Ok(validated_rrs) = validated_rrs {
418				let block_time = self.latest_block_time.load(Ordering::Acquire) as u64;
419				// Block times may be up to two hours in the future and some time into the past
420				// (we assume no more than two hours, though the actual limits are rather
421				// complicated).
422				// Thus, we have to let the proof times be rather fuzzy.
423				if validated_rrs.valid_from > block_time + 60 * 2 {
424					return None;
425				}
426				if validated_rrs.expires < block_time - 60 * 2 {
427					return None;
428				}
429				let resolved_rrs = validated_rrs.resolve_name(&entry.key());
430				if resolved_rrs.is_empty() {
431					return None;
432				}
433
434				let (_, requests) = entry.remove_entry();
435
436				const URI_PREFIX: &str = "bitcoin:";
437				let mut candidate_records = resolved_rrs
438					.iter()
439					.filter_map(
440						|rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None },
441					)
442					.filter_map(
443						|data| if let Ok(s) = String::from_utf8(data) { Some(s) } else { None },
444					)
445					.filter(|data_string| data_string.len() > URI_PREFIX.len())
446					.filter(|data_string| {
447						data_string[..URI_PREFIX.len()].eq_ignore_ascii_case(URI_PREFIX)
448					});
449				// Check that there is exactly one TXT record that begins with
450				// bitcoin: as required by BIP 353 (and is valid UTF-8).
451				match (candidate_records.next(), candidate_records.next()) {
452					(Some(txt), None) => {
453						let completed_requests =
454							requests.into_iter().map(|r| (r.name, r.payment_id)).collect();
455						return Some((completed_requests, txt));
456					},
457					_ => {},
458				}
459			}
460		}
461		None
462	}
463}