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.