From fa18452da360df6191b6daa7b16c03b27b3ca562 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 2 Sep 2026 22:14:13 -0400 Subject: [PATCH] fix(didbot-dns): apply the same wildcard-depth fix to Route53Dns Route53Dns::accepts had the identical ends_with shape WildcardDns::accepts did before it: any depth under the zone was accepted, not just the one label a wildcard record actually matches. This is the production DNS backend, so the flaw was live where it matters most. Reused the same hostname_is_at_or_below-plus-label-count check. resync's own live-zone test used a TXT fixture two labels below the zone (`_acme-challenge.a.`), a shape didbot_tls::acme never produces -- the real DNS-01 challenge for both the apex and wildcard identifiers always collapses onto `_acme-challenge.`, one label down. Moved the fixture there rather than loosen accepts() to keep a synthetic shape working. Mutation-tested: reverting accepts() to the old ends_with check lets the new over-deep publish test's host through to an unrelated UnsupportedTarget error instead of NotUnderZone; restoring it refuses correctly. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: If1fdfbdfa2c31d40552f8d374a3052fe7d38aed3 --- crates/didbot-dns/src/route53.rs | 42 +++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/didbot-dns/src/route53.rs b/crates/didbot-dns/src/route53.rs index 609c3d5c..8393ad0f 100644 --- a/crates/didbot-dns/src/route53.rs +++ b/crates/didbot-dns/src/route53.rs @@ -91,6 +91,7 @@ //! those keys the same way. use crate::{CaaRecords, CaaValue, DnsError, DnsProvider, RecordTarget, Records, TxtRecords}; +use didbot_identity::hostname_is_at_or_below; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::collections::BTreeSet; @@ -375,8 +376,20 @@ impl Route53Dns { } /// Whether `host` is under this provider's zone. + /// + /// A deployment answers this zone with a single-label wildcard DNS + /// record (`*.`), and RFC 1034 wildcards match exactly one label. + /// So `host` must be the zone itself (the apex, for `_acme-challenge` + /// and the like) or exactly one label below it — never two or more; see + /// [`crate::WildcardDns::accepts`], which mirrors this exactly for the + /// same reason. pub fn accepts(&self, host: &str) -> bool { - host == self.zone || host.ends_with(&format!(".{}", self.zone)) + if !hostname_is_at_or_below(host, &self.zone) { + return false; + } + let zone_labels = self.zone.matches('.').count() + 1; + let host_labels = host.matches('.').count() + 1; + host_labels <= zone_labels + 1 } fn check(&self, host: &str) -> Result<(), DnsError> { @@ -1419,6 +1432,22 @@ mod tests { ); } + #[test] + fn publish_refuses_a_host_more_than_one_label_below_the_zone() { + // Route53Dns answers the zone with a single-label wildcard record; + // a host two labels down would never resolve to a request this + // deployment could serve. No transport call is queued, so this + // would also panic on an unexpected HTTP call if `accepts` failed + // to refuse before `publish` reached the network. + let dns = provider(vec![]); + assert_eq!( + dns.publish("deep.a.agents.example.com", &RecordTarget::Loopback), + Err(DnsError::NotUnderZone { + host: "deep.a.agents.example.com".to_string() + }) + ); + } + #[test] fn publish_refuses_loopback_as_unsupported() { let dns = provider(vec![]); @@ -1708,10 +1737,17 @@ mod tests { #[test] fn resync_hydrates_local_bookkeeping_from_a_live_zone() { + // The TXT fixture sits at `_acme-challenge.` -- one label + // below the zone, exactly where `didbot_tls::acme` always publishes + // the DNS-01 challenge for both the apex and the wildcard + // identifier (see `challenge_record_name`). A per-agent name two + // labels below the zone is not a shape this deployment ever + // produces, and `accepts` now refuses it the same way it refuses a + // handle that deep. let list_body = "\ a.agents.example.com.A300\ 10.0.0.1\ - _acme-challenge.a.agents.example.com.TXT60\ + _acme-challenge.agents.example.com.TXT60\ "tok"\ false"; let transport = Box::new(FakeTransport::new(vec![ok(list_body)])); @@ -1719,7 +1755,7 @@ mod tests { assert!(dns.resync().is_ok()); assert_eq!(dns.published(), vec!["a.agents.example.com"]); assert_eq!( - dns.txt_values("_acme-challenge.a.agents.example.com"), + dns.txt_values("_acme-challenge.agents.example.com"), vec!["tok"] ); // A record now known locally collides on a second real publish. -- 2.51.2