From dfcedfd01fd5e891642bd7214df34bac4fcb57f0 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Thu, 3 Sep 2026 00:51:44 -0400 Subject: [PATCH] refactor: share the RFC 1034 wildcard depth rule across dns and pds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hostname_is_at_or_one_label_below` joins `hostname_is_at_or_below` in didbot-identity, and the three call sites that each spelled the label arithmetic out — WildcardDns::accepts, Route53Dns::accepts and Provisioner::check_requested_handle — now call it. The two error types stay distinct; only the predicate is shared. Co-Authored-By: Claude Opus 5 (1M context) --- crates/didbot-dns/src/lib.rs | 18 +++++++----------- crates/didbot-dns/src/route53.rs | 16 ++++++---------- crates/didbot-identity/src/did.rs | 20 ++++++++++++++++++++ crates/didbot-identity/src/lib.rs | 5 +++-- crates/didbot-pds/src/provision.rs | 28 +++++++++++++++------------- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/crates/didbot-dns/src/lib.rs b/crates/didbot-dns/src/lib.rs index 2eb96074..da61167f 100644 --- a/crates/didbot-dns/src/lib.rs +++ b/crates/didbot-dns/src/lib.rs @@ -44,7 +44,7 @@ use std::fmt; use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::Mutex; -use didbot_identity::hostname_is_at_or_below; +use didbot_identity::{hostname_is_at_or_below, hostname_is_at_or_one_label_below}; #[cfg(feature = "route53")] pub mod route53; @@ -923,17 +923,13 @@ impl WildcardDns { /// could ever route a request to, unreachable rather than merely /// unpublished. /// - /// [`hostname_is_at_or_below`] alone would accept any depth, so this - /// pairs it with a label-count check the same way - /// `Provisioner::check_requested_handle` does for a caller-asserted - /// handle. + /// [`hostname_is_at_or_below`] alone would accept any depth, so the + /// wildcard rule itself lives in + /// [`hostname_is_at_or_one_label_below`] — the same predicate + /// `Provisioner::check_requested_handle` applies to a caller-asserted + /// handle, so the two gates cannot drift at the apex. pub fn accepts(&self, host: &str) -> bool { - 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 + hostname_is_at_or_one_label_below(host, &self.zone) } fn check(&self, host: &str) -> Result<(), DnsError> { diff --git a/crates/didbot-dns/src/route53.rs b/crates/didbot-dns/src/route53.rs index 8393ad0f..6a4bf858 100644 --- a/crates/didbot-dns/src/route53.rs +++ b/crates/didbot-dns/src/route53.rs @@ -91,7 +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 didbot_identity::hostname_is_at_or_one_label_below; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::collections::BTreeSet; @@ -380,16 +380,12 @@ impl Route53Dns { /// 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. + /// and the like) or exactly one label below it — never two or more. + /// The rule itself is [`hostname_is_at_or_one_label_below`], shared with + /// [`crate::WildcardDns::accepts`] and with the PDS-side handle check + /// rather than spelled out a third time here. pub fn accepts(&self, host: &str) -> bool { - 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 + hostname_is_at_or_one_label_below(host, &self.zone) } fn check(&self, host: &str) -> Result<(), DnsError> { diff --git a/crates/didbot-identity/src/did.rs b/crates/didbot-identity/src/did.rs index 55a9b46e..bba363ab 100644 --- a/crates/didbot-identity/src/did.rs +++ b/crates/didbot-identity/src/did.rs @@ -308,6 +308,26 @@ pub fn hostname_is_at_or_below(host: &str, ancestor: &str) -> bool { .all(|(a, b)| a.eq_ignore_ascii_case(b)) } +/// Whether `host` is `ancestor` or exactly one DNS label below it. +/// +/// The RFC 1034 wildcard rule, as one predicate. A deployment answers its +/// zone with a single-label wildcard record (`*.`), and such a record +/// matches exactly one label — so a name this deployment can actually route +/// to is either the zone apex itself (reachable without any wildcard, and +/// where `_acme-challenge` and other service records live) or one label +/// below it. Two or more labels below is unreachable rather than merely +/// unpublished. +/// +/// A strict refinement of [`hostname_is_at_or_below`], and inherits its +/// case-insensitivity and its label-boundary safety: `evilagents.localhost` +/// is not one label below `agents.localhost`, it is not under it at all. +pub fn hostname_is_at_or_one_label_below(host: &str, ancestor: &str) -> bool { + if !hostname_is_at_or_below(host, ancestor) { + return false; + } + host.matches('.').count() <= ancestor.matches('.').count() + 1 +} + /// A validated `did:web` belonging to one agent. /// /// Held as the exact DID string plus its decoded authority, so that neither diff --git a/crates/didbot-identity/src/lib.rs b/crates/didbot-identity/src/lib.rs index bdd572bd..53393752 100644 --- a/crates/didbot-identity/src/lib.rs +++ b/crates/didbot-identity/src/lib.rs @@ -44,8 +44,9 @@ pub mod handle; pub mod resolve; pub use did::{ - canonical_did, hostname_is_at_or_below, validate_did, validate_label, AgentDid, DidError, - SyntaxError, Zone, ZoneRegistry, ZoneRegistryError, MAX_DID_LENGTH, + canonical_did, hostname_is_at_or_below, hostname_is_at_or_one_label_below, validate_did, + validate_label, AgentDid, DidError, SyntaxError, Zone, ZoneRegistry, ZoneRegistryError, + MAX_DID_LENGTH, }; pub use document::{DidDocument, Service, VerificationMethod}; pub use handle::{ diff --git a/crates/didbot-pds/src/provision.rs b/crates/didbot-pds/src/provision.rs index 9410958c..37b58711 100644 --- a/crates/didbot-pds/src/provision.rs +++ b/crates/didbot-pds/src/provision.rs @@ -7,7 +7,8 @@ use std::sync::{Arc, Mutex}; use didbot_attest::{Assurance, Provenance}; use didbot_dns::{DnsError, DnsProvider, RecordTarget}; use didbot_identity::{ - hostname_is_at_or_below, validate_handle, AgentDid, DidDocument, DidError, HandleError, Zone, + hostname_is_at_or_below, hostname_is_at_or_one_label_below, validate_handle, AgentDid, + DidDocument, DidError, HandleError, Zone, }; use didbot_key::SigningKey; use time::OffsetDateTime; @@ -2703,18 +2704,19 @@ where // because the two failures should stay distinguishable if a // deployment ever needs to tell them apart. // - // At or below by *one* label, not exactly one: the zone apex has no - // label below the zone and is reachable without a wildcard at all, - // so the depth rule has nothing to say about it. Refusing it here - // would answer `HandleEscapesZone` for a hostname that plainly does - // not escape the zone, and would do so ahead of the question that - // actually settles the apex — whether anyone already holds it, which - // on any deployment that has adopted its own account is the server. - // `didbot_dns::WildcardDns::accepts` draws the same boundary, for - // the apex records (`_acme-challenge` and the like) it has to serve. - let zone_labels = zone.host().matches('.').count() + 1; - let handle_labels = handle.matches('.').count() + 1; - if handle_labels > zone_labels + 1 { + // The apex passes: it has no label below the zone and is reachable + // without a wildcard at all, so the depth rule has nothing to say + // about it. Refusing it here would answer `HandleEscapesZone` for a + // hostname that plainly does not escape the zone, and would do so + // ahead of the question that actually settles the apex — whether + // anyone already holds it, which on any deployment that has adopted + // its own account is the server. + // + // The predicate is shared with the DNS providers that publish these + // names (`didbot_dns::WildcardDns::accepts`), so the gate that + // admits a handle and the gate that publishes it cannot disagree + // about where the boundary sits. + if !hostname_is_at_or_one_label_below(handle, zone.host()) { return Err(ProvisionError::HandleEscapesZone { handle: handle.to_owned(), zone: zone.host().to_owned(), -- 2.51.2