Skip to content

Commit 30626ff

Browse files
committed
Expose DNS query failure on invalid proofs or bad BIP 353 records
In the previous commit we started handling the new `DNSSECError` onion messages and using them to expose when a BIP 353 resolution over onion messages should be considered failed due to all of our queries having filed. However, queries can also fail if all of our queries either errored or returned bogus proofs, or if we received a valid proof which proved there is no BIP 353 record or `Offer`. Here we consider such failures and expose them as well.
1 parent bd237b1 commit 30626ff

1 file changed

Lines changed: 113 additions & 57 deletions

File tree

lightning/src/onion_message/dns_resolution.rs

Lines changed: 113 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,14 @@ impl OMNameResolver {
522522
///
523523
/// If an [`Offer`] is found, it, as well as the [`PaymentId`] and original `name` passed to
524524
/// [`Self::resolve_name`] are returned.
525+
///
526+
/// If the proof is invalid and there are no remaining queries for this name, or if the proof is
527+
/// valid and does not contain a valid BIP 353 entry or BOLT 12 [`Offer`], `Err` will be
528+
/// returned with the set of [`HumanReadableName`] and [`PaymentId`] requests which should be
529+
/// considered failed.
525530
pub fn handle_dnssec_proof_for_offer(
526531
&self, msg: DNSSECProof, context: DNSResolverContext,
527-
) -> Option<(Vec<(HumanReadableName, PaymentId)>, Offer)> {
532+
) -> Result<(Vec<(HumanReadableName, PaymentId)>, Offer), Vec<(HumanReadableName, PaymentId)>> {
528533
let (completed_requests, uri) = self.handle_dnssec_proof_for_uri(msg, context)?;
529534
if let Some((_onchain, params)) = uri.split_once('?') {
530535
for param in params.split('&') {
@@ -535,13 +540,13 @@ impl OMNameResolver {
535540
};
536541
if k.eq_ignore_ascii_case("lno") {
537542
if let Ok(offer) = Offer::from_str(v) {
538-
return Some((completed_requests, offer));
543+
return Ok((completed_requests, offer));
539544
}
540-
return None;
545+
return Err(Vec::new());
541546
}
542547
}
543548
}
544-
None
549+
Err(Vec::new())
545550
}
546551

547552
/// Handles a [`DNSSECProof`] message, attempting to verify it and match it against any pending
@@ -553,14 +558,18 @@ impl OMNameResolver {
553558
/// Note that a single proof for a wildcard DNS entry may complete several requests for
554559
/// different [`HumanReadableName`]s.
555560
///
561+
/// If the proof is invalid and there are no remaining queries for this name, or if the proof is
562+
/// valid and does not contain a valid BIP 353 entry, `Err` will be returned with the set of
563+
/// [`HumanReadableName`] and [`PaymentId`] requests which should be considered failed.
564+
///
556565
/// This method is useful for those who handle bitcoin: URIs already, handling more than just
557566
/// BOLT12 [`Offer`]s.
558567
pub fn handle_dnssec_proof_for_uri(
559568
&self, msg: DNSSECProof, context: DNSResolverContext,
560-
) -> Option<(Vec<(HumanReadableName, PaymentId)>, String)> {
569+
) -> Result<(Vec<(HumanReadableName, PaymentId)>, String), Vec<(HumanReadableName, PaymentId)>> {
561570
let DNSSECProof { name: answer_name, proof } = msg;
562571
let mut pending_resolves = self.pending_resolves.lock().unwrap();
563-
if let hash_map::Entry::Occupied(entry) = pending_resolves.entry(answer_name) {
572+
if let hash_map::Entry::Occupied(mut entry) = pending_resolves.entry(answer_name) {
564573
if !entry.get().iter().any(|query| query.context == context) {
565574
// If we don't have any pending queries with the context included in the blinded
566575
// path (implying someone sent us this response not using the blinded path we gave
@@ -570,66 +579,113 @@ impl OMNameResolver {
570579
// If there was at least one query with the same context, we go ahead and complete
571580
// all queries for the same name, as there's no point in waiting for another proof
572581
// for the same name.
573-
return None;
582+
return Err(Vec::new());
574583
}
575-
let parsed_rrs = parse_rr_stream(&proof);
576-
let validated_rrs =
577-
parsed_rrs.as_ref().and_then(|rrs| verify_rr_stream(rrs).map_err(|_| &()));
578-
if let Ok(validated_rrs) = validated_rrs {
579-
#[allow(unused_assignments, unused_mut)]
580-
let mut time = self.latest_block_time.load(Ordering::Acquire) as u64;
581-
#[cfg(all(feature = "std", not(fuzzing)))]
582-
{
583-
use std::time::{SystemTime, UNIX_EPOCH};
584-
let now = SystemTime::now().duration_since(UNIX_EPOCH);
585-
time = now.expect("Time must be > 1970").as_secs();
586-
}
587-
if time != 0 {
588-
// Block times may be up to two hours in the future and some time into the past
589-
// (we assume no more than two hours, though the actual limits are rather
590-
// complicated).
591-
// Thus, we have to let the proof times be rather fuzzy.
592-
let max_time_offset =
593-
if cfg!(all(feature = "std", not(fuzzing))) { 0 } else { 60 * 2 };
594-
if validated_rrs.valid_from > time + max_time_offset {
595-
return None;
584+
let mut valid_resolution = false;
585+
let res = parse_rr_stream(&proof)
586+
.and_then(|rrs| {
587+
if let Some(verified_rrs) = self.verify_dnssec_proof_for_rrs(entry.key(), &rrs) {
588+
valid_resolution = true;
589+
self.map_rrs_to_uri(verified_rrs).ok_or(())
590+
} else {
591+
Err(())
596592
}
597-
if validated_rrs.expires < time - max_time_offset {
598-
return None;
593+
});
594+
if valid_resolution {
595+
let requests = entry.remove_entry();
596+
match res {
597+
Ok(txt) => {
598+
Ok((requests.1.into_iter().map(|r| (r.name, r.payment_id)).collect(), txt))
599599
}
600+
Err(()) => Err(Vec::new()),
600601
}
601-
let resolved_rrs = validated_rrs.resolve_name(&entry.key());
602-
if resolved_rrs.is_empty() {
603-
return None;
602+
} else {
603+
let mut failed_resolutions = Vec::new();
604+
entry.get_mut().retain_mut(|query| {
605+
if query.context == context {
606+
query.pending_queries = query.pending_queries.saturating_sub(1);
607+
if query.pending_queries == 0 {
608+
failed_resolutions.push((query.name, query.payment_id));
609+
false
610+
} else {
611+
true
612+
}
613+
} else {
614+
true
615+
}
616+
});
617+
618+
if entry.get().is_empty() {
619+
entry.remove_entry();
604620
}
621+
Err(failed_resolutions)
622+
}
623+
} else {
624+
Err(Vec::new())
625+
}
626+
}
605627

606-
let (_, requests) = entry.remove_entry();
607-
608-
const URI_PREFIX: &str = "bitcoin:";
609-
let mut candidate_records = resolved_rrs
610-
.iter()
611-
.filter_map(
612-
|rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None },
613-
)
614-
.filter_map(|data| String::from_utf8(data).ok())
615-
.filter(|data_string| data_string.len() > URI_PREFIX.len())
616-
.filter(|data_string| {
617-
let pfx = &data_string.as_bytes()[..URI_PREFIX.len()];
618-
pfx.eq_ignore_ascii_case(URI_PREFIX.as_bytes())
619-
});
620-
// Check that there is exactly one TXT record that begins with
621-
// bitcoin: as required by BIP 353 (and is valid UTF-8).
622-
match (candidate_records.next(), candidate_records.next()) {
623-
(Some(txt), None) => {
624-
let completed_requests =
625-
requests.into_iter().map(|r| (r.name, r.payment_id)).collect();
626-
return Some((completed_requests, txt));
627-
},
628-
_ => {},
628+
fn verify_dnssec_proof_for_rrs<'a>(
629+
&self, resolved_name: &Name, rrs: &'a [RR],
630+
) -> Option<Vec<&'a RR>> {
631+
let validated_rrs = verify_rr_stream(rrs);
632+
if let Ok(validated_rrs) = validated_rrs {
633+
#[allow(unused_assignments, unused_mut)]
634+
let mut time = self.latest_block_time.load(Ordering::Acquire) as u64;
635+
#[cfg(all(feature = "std", not(fuzzing)))]
636+
{
637+
use std::time::{SystemTime, UNIX_EPOCH};
638+
let now = SystemTime::now().duration_since(UNIX_EPOCH);
639+
time = now.expect("Time must be > 1970").as_secs();
640+
}
641+
if time != 0 {
642+
// Block times may be up to two hours in the future and some time into the past
643+
// (we assume no more than two hours, though the actual limits are rather
644+
// complicated).
645+
// Thus, we have to let the proof times be rather fuzzy.
646+
let max_time_offset =
647+
if cfg!(all(feature = "std", not(fuzzing))) { 0 } else { 60 * 2 };
648+
if validated_rrs.valid_from > time + max_time_offset {
649+
return None;
650+
}
651+
if validated_rrs.expires < time - max_time_offset {
652+
return None;
629653
}
630654
}
655+
let resolved_rrs = validated_rrs.resolve_name(resolved_name);
656+
if resolved_rrs.is_empty() {
657+
return None;
658+
}
659+
660+
Some(resolved_rrs)
661+
} else {
662+
None
663+
}
664+
}
665+
666+
fn map_rrs_to_uri(
667+
&self, resolved_rrs: Vec<&RR>,
668+
) -> Option<String> {
669+
const URI_PREFIX: &str = "bitcoin:";
670+
let mut candidate_records = resolved_rrs
671+
.iter()
672+
.filter_map(
673+
|rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None },
674+
)
675+
.filter_map(|data| String::from_utf8(data).ok())
676+
.filter(|data_string| data_string.len() > URI_PREFIX.len())
677+
.filter(|data_string| {
678+
let pfx = &data_string.as_bytes()[..URI_PREFIX.len()];
679+
pfx.eq_ignore_ascii_case(URI_PREFIX.as_bytes())
680+
});
681+
// Check that there is exactly one TXT record that begins with
682+
// bitcoin: as required by BIP 353 (and is valid UTF-8).
683+
match (candidate_records.next(), candidate_records.next()) {
684+
(Some(txt), None) => {
685+
Some(txt)
686+
},
687+
_ => None,
631688
}
632-
None
633689
}
634690

635691
/// Handles a [`DNSSECError`] message, indicating that one of the resolvers we sent a

0 commit comments

Comments
 (0)