Skip to main content

hns_namespace_resolution/
lib.rs

1//! Shared full-host namespace decision contract for dual-root browsers.
2//!
3//! Network adapters independently build one complete, validated origin plan
4//! for HNS and ICANN. This crate compares those plans, preserves authenticated
5//! absence separately from failures, applies explicit divergence precedence,
6//! and produces a query- and policy-bound decision fingerprint for connection
7//! and cache isolation. Plans retain distinct origin-alias, ServiceMode target,
8//! endpoint-CNAME, address, and TLS trust state and never mix roots.
9//!
10//! The IANA root-zone list is intentionally absent. A suffix snapshot may
11//! order network work outside this crate, but it can never create a namespace
12//! decision.
13
14#![forbid(unsafe_code)]
15#![allow(
16    clippy::doc_markdown,
17    clippy::module_name_repetitions,
18    reason = "DNSSEC, HNS, ICANN, TLSA, SVCB, ALPN, ECH, and WebPKI are protocol names"
19)]
20
21use std::fmt;
22use std::net::{IpAddr, SocketAddr};
23use std::num::NonZeroU16;
24use std::str::FromStr;
25
26/// Comparison schema included in cache and decision identities.
27pub const COMPARISON_SCHEMA_VERSION: u16 = 1;
28/// Maximum canonical DNS presentation-name length without a root dot.
29pub const MAX_HOST_BYTES: usize = 253;
30/// Maximum aliases retained in each origin or endpoint path in one root plan.
31pub const MAX_ALIAS_STEPS: usize = 8;
32/// Maximum number of usable endpoints retained in one root plan.
33pub const MAX_ENDPOINTS: usize = 32;
34/// Maximum number of ALPN identifiers retained in one service binding.
35pub const MAX_ALPN_IDS: usize = 16;
36/// Maximum bytes in one ALPN identifier.
37pub const MAX_ALPN_ID_BYTES: usize = 255;
38/// Maximum number of SVCB parameters retained in one service binding.
39pub const MAX_SERVICE_PARAMETERS: usize = 32;
40/// Maximum bytes retained in one SVCB parameter value or ECH configuration.
41pub const MAX_PARAMETER_BYTES: usize = 65_535;
42/// Maximum number of canonical TLSA records retained in one root plan.
43pub const MAX_TLSA_RECORDS: usize = 32;
44
45/// DNS namespace whose independently validated plan may be selected.
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47#[repr(u8)]
48pub enum Namespace {
49    /// Handshake root.
50    Hns = 1,
51    /// ICANN root.
52    Icann = 2,
53}
54
55impl Namespace {
56    fn tag(self) -> u8 {
57        self as u8
58    }
59}
60
61/// Canonical Handshake network identity used for proof and cache isolation.
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63#[repr(u8)]
64pub enum HnsNetwork {
65    /// Handshake mainnet.
66    Mainnet = 0,
67    /// Handshake testnet.
68    Testnet = 1,
69    /// Handshake regtest.
70    Regtest = 2,
71    /// Handshake simnet.
72    Simnet = 3,
73}
74
75impl HnsNetwork {
76    fn tag(self) -> u8 {
77        self as u8
78    }
79}
80
81/// Canonical lower-case ASCII DNS hostname without a root dot.
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct CanonicalHost(String);
84
85impl CanonicalHost {
86    /// Parses a host after the platform URL parser has applied IDNA.
87    ///
88    /// IP literals, whitespace, empty labels, underscore labels, and names
89    /// outside DNS presentation bounds are rejected.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`ValidationError::InvalidHost`] for a non-DNS hostname.
94    pub fn parse(input: &str) -> Result<Self, ValidationError> {
95        if input.trim() != input || input.is_empty() || !input.is_ascii() {
96            return Err(ValidationError::InvalidHost);
97        }
98        let without_root = input.strip_suffix('.').unwrap_or(input);
99        if without_root.is_empty()
100            || without_root.ends_with('.')
101            || without_root.len() > MAX_HOST_BYTES
102            || IpAddr::from_str(without_root).is_ok()
103        {
104            return Err(ValidationError::InvalidHost);
105        }
106        let normalized = without_root.to_ascii_lowercase();
107        for label in normalized.split('.') {
108            if label.is_empty()
109                || label.len() > 63
110                || label.starts_with('-')
111                || label.ends_with('-')
112                || !label
113                    .bytes()
114                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
115            {
116                return Err(ValidationError::InvalidHost);
117            }
118        }
119        Ok(Self(normalized))
120    }
121
122    /// Returns the canonical host.
123    #[must_use]
124    pub fn as_str(&self) -> &str {
125        &self.0
126    }
127}
128
129impl fmt::Display for CanonicalHost {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        formatter.write_str(self.as_str())
132    }
133}
134
135/// Browser origin scheme whose effective endpoint is classified.
136#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137#[repr(u8)]
138pub enum OriginScheme {
139    /// Cleartext HTTP.
140    Http = 1,
141    /// TLS HTTP.
142    Https = 2,
143    /// Cleartext WebSocket.
144    Ws = 3,
145    /// TLS WebSocket.
146    Wss = 4,
147}
148
149impl OriginScheme {
150    /// Default port for this scheme.
151    #[must_use]
152    pub const fn default_port(self) -> NonZeroU16 {
153        match self {
154            Self::Http | Self::Ws => nonzero_port(80),
155            Self::Https | Self::Wss => nonzero_port(443),
156        }
157    }
158
159    /// Whether the origin uses TLS.
160    #[must_use]
161    pub const fn uses_tls(self) -> bool {
162        matches!(self, Self::Https | Self::Wss)
163    }
164
165    fn tag(self) -> u8 {
166        self as u8
167    }
168}
169
170const fn nonzero_port(value: u16) -> NonZeroU16 {
171    match NonZeroU16::new(value) {
172        Some(value) => value,
173        None => unreachable!(),
174    }
175}
176
177/// Application protocol selected for an origin connection.
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179#[repr(u8)]
180pub enum ApplicationProtocol {
181    /// HTTP/1.1, including ordinary WebSocket Upgrade.
182    Http11 = 1,
183    /// HTTP/2.
184    Http2 = 2,
185    /// HTTP/3 over QUIC.
186    Http3 = 3,
187}
188
189impl ApplicationProtocol {
190    fn tag(self) -> u8 {
191        self as u8
192    }
193}
194
195/// Protocols supported by the requesting browser transport.
196#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
197pub struct ProtocolCapabilities {
198    http11: bool,
199    http2: bool,
200    http3: bool,
201}
202
203impl ProtocolCapabilities {
204    /// Creates one non-empty protocol capability set.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`ValidationError::InvalidQuery`] when all protocols are off.
209    pub const fn new(http11: bool, http2: bool, http3: bool) -> Result<Self, ValidationError> {
210        if http11 || http2 || http3 {
211            Ok(Self {
212                http11,
213                http2,
214                http3,
215            })
216        } else {
217            Err(ValidationError::InvalidQuery)
218        }
219    }
220
221    /// Capabilities used by a browser supporting HTTP/1.1, HTTP/2, and HTTP/3.
222    #[must_use]
223    pub const fn all() -> Self {
224        Self {
225            http11: true,
226            http2: true,
227            http3: true,
228        }
229    }
230
231    /// Whether the selected protocol is supported.
232    #[must_use]
233    pub const fn supports(self, protocol: ApplicationProtocol) -> bool {
234        match protocol {
235            ApplicationProtocol::Http11 => self.http11,
236            ApplicationProtocol::Http2 => self.http2,
237            ApplicationProtocol::Http3 => self.http3,
238        }
239    }
240
241    fn flags(self) -> u8 {
242        u8::from(self.http11) | (u8::from(self.http2) << 1) | (u8::from(self.http3) << 2)
243    }
244}
245
246/// Canonical origin input common to both root lookups.
247#[derive(Clone, Debug, Eq, Hash, PartialEq)]
248pub struct OriginQuery {
249    host: CanonicalHost,
250    scheme: OriginScheme,
251    explicit_port: Option<NonZeroU16>,
252    supported_protocols: ProtocolCapabilities,
253}
254
255impl OriginQuery {
256    /// Creates a canonical origin query.
257    #[must_use]
258    pub const fn new(
259        host: CanonicalHost,
260        scheme: OriginScheme,
261        explicit_port: Option<NonZeroU16>,
262        supported_protocols: ProtocolCapabilities,
263    ) -> Self {
264        Self {
265            host,
266            scheme,
267            explicit_port,
268            supported_protocols,
269        }
270    }
271
272    /// Canonical DNS host.
273    #[must_use]
274    pub const fn host(&self) -> &CanonicalHost {
275        &self.host
276    }
277
278    /// Browser origin scheme.
279    #[must_use]
280    pub const fn scheme(&self) -> OriginScheme {
281        self.scheme
282    }
283
284    /// Explicit URL port, if present.
285    #[must_use]
286    pub const fn explicit_port(&self) -> Option<NonZeroU16> {
287        self.explicit_port
288    }
289
290    /// URL port before HTTPS/SVCB service selection.
291    #[must_use]
292    pub const fn origin_port(&self) -> NonZeroU16 {
293        match self.explicit_port {
294            Some(port) => port,
295            None => self.scheme.default_port(),
296        }
297    }
298
299    /// Supported origin protocols.
300    #[must_use]
301    pub const fn supported_protocols(&self) -> ProtocolCapabilities {
302        self.supported_protocols
303    }
304}
305
306/// Alias mechanism represented by one retained origin-plan step.
307#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
308#[repr(u8)]
309pub enum AliasKind {
310    /// DNS CNAME.
311    Cname = 1,
312    /// HTTPS/SVCB AliasMode.
313    Https = 2,
314}
315
316impl AliasKind {
317    fn tag(self) -> u8 {
318        self as u8
319    }
320}
321
322/// One canonical alias edge inside a single root lookup.
323#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
324pub struct AliasStep {
325    kind: AliasKind,
326    owner: CanonicalHost,
327    target: CanonicalHost,
328}
329
330impl AliasStep {
331    /// Creates a non-self-referential alias step.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ValidationError::InvalidPlan`] for a self-alias.
336    pub fn new(
337        kind: AliasKind,
338        owner: CanonicalHost,
339        target: CanonicalHost,
340    ) -> Result<Self, ValidationError> {
341        if owner == target {
342            return Err(ValidationError::InvalidPlan);
343        }
344        Ok(Self {
345            kind,
346            owner,
347            target,
348        })
349    }
350
351    /// Alias mechanism.
352    #[must_use]
353    pub const fn kind(&self) -> AliasKind {
354        self.kind
355    }
356
357    /// Canonical alias owner.
358    #[must_use]
359    pub const fn owner(&self) -> &CanonicalHost {
360        &self.owner
361    }
362
363    /// Canonical alias target.
364    #[must_use]
365    pub const fn target(&self) -> &CanonicalHost {
366        &self.target
367    }
368}
369
370/// Effective service transport for TLSA and origin connection state.
371#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
372#[repr(u8)]
373pub enum ServiceTransport {
374    /// TCP.
375    Tcp = 1,
376    /// UDP, including QUIC.
377    Udp = 2,
378}
379
380impl ServiceTransport {
381    fn tag(self) -> u8 {
382        self as u8
383    }
384}
385
386/// Canonical SVCB parameter retained for equivalence comparison.
387#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
388pub struct ServiceParameter {
389    key: u16,
390    value: Vec<u8>,
391}
392
393impl ServiceParameter {
394    /// Creates one bounded SVCB parameter.
395    ///
396    /// # Errors
397    ///
398    /// Returns [`ValidationError::BoundExceeded`] for an oversized value.
399    pub fn new(key: u16, value: Vec<u8>) -> Result<Self, ValidationError> {
400        if value.len() > MAX_PARAMETER_BYTES {
401            return Err(ValidationError::BoundExceeded);
402        }
403        Ok(Self { key, value })
404    }
405
406    /// Numeric SVCB key.
407    #[must_use]
408    pub const fn key(&self) -> u16 {
409        self.key
410    }
411
412    /// Canonical wire value.
413    #[must_use]
414    pub fn value(&self) -> &[u8] {
415        &self.value
416    }
417}
418
419/// Unvalidated service-binding input normalized by [`ServiceBinding::new`].
420#[derive(Clone, Debug, Eq, PartialEq)]
421pub struct ServiceBindingInput {
422    /// Selected HTTPS/SVCB priority, or `None` when no binding applies.
423    pub priority: Option<u16>,
424    /// Effective ServiceMode target after expanding a root (`.`) TargetName to
425    /// the alias-terminal owner.
426    pub service_target: CanonicalHost,
427    /// Canonical sorted mandatory parameter keys.
428    pub mandatory_keys: Vec<u16>,
429    /// Raw advertised ALPN identifiers. Implicit scheme defaults are not
430    /// inserted into this list.
431    pub advertised_alpn: Vec<Vec<u8>>,
432    /// Protocol selected from the browser capability set.
433    pub selected_protocol: ApplicationProtocol,
434    /// Effective origin service port after HTTPS/SVCB selection.
435    pub effective_port: NonZeroU16,
436    /// Effective service transport.
437    pub transport: ServiceTransport,
438    /// Connection-used IPv4/IPv6 hints.
439    pub connection_hints: Vec<IpAddr>,
440    /// Supported ECH configuration used by the connection, if any.
441    pub ech_config: Option<Vec<u8>>,
442    /// Canonical supported SVCB parameters, including connection-affecting
443    /// parameters not represented by a dedicated field.
444    pub parameters: Vec<ServiceParameter>,
445}
446
447/// Normalized connection-affecting HTTPS/SVCB policy.
448#[derive(Clone, Debug, Eq, Hash, PartialEq)]
449pub struct ServiceBinding {
450    priority: Option<u16>,
451    service_target: CanonicalHost,
452    mandatory_keys: Vec<u16>,
453    advertised_alpn: Vec<Vec<u8>>,
454    selected_protocol: ApplicationProtocol,
455    effective_port: NonZeroU16,
456    transport: ServiceTransport,
457    connection_hints: Vec<IpAddr>,
458    ech_config: Option<Vec<u8>>,
459    parameters: Vec<ServiceParameter>,
460}
461
462impl ServiceBinding {
463    /// Normalizes and validates one service binding.
464    ///
465    /// # Errors
466    ///
467    /// Returns [`ValidationError::BoundExceeded`] or
468    /// [`ValidationError::InvalidPlan`] for malformed or duplicate data.
469    pub fn new(mut input: ServiceBindingInput) -> Result<Self, ValidationError> {
470        if input.advertised_alpn.len() > MAX_ALPN_IDS
471            || input.mandatory_keys.len() > MAX_SERVICE_PARAMETERS
472            || input.connection_hints.len() > MAX_ENDPOINTS
473            || input.parameters.len() > MAX_SERVICE_PARAMETERS
474            || input
475                .advertised_alpn
476                .iter()
477                .any(|id| id.is_empty() || id.len() > MAX_ALPN_ID_BYTES)
478            || input
479                .ech_config
480                .as_ref()
481                .is_some_and(|value| value.is_empty() || value.len() > MAX_PARAMETER_BYTES)
482        {
483            return Err(ValidationError::BoundExceeded);
484        }
485        if !matches!(
486            (input.selected_protocol, input.transport),
487            (
488                ApplicationProtocol::Http11 | ApplicationProtocol::Http2,
489                ServiceTransport::Tcp
490            ) | (ApplicationProtocol::Http3, ServiceTransport::Udp)
491        ) {
492            return Err(ValidationError::InvalidPlan);
493        }
494        if input.priority == Some(0)
495            || (input.priority.is_none()
496                && (!input.mandatory_keys.is_empty()
497                    || !input.advertised_alpn.is_empty()
498                    || !input.connection_hints.is_empty()
499                    || input.ech_config.is_some()
500                    || !input.parameters.is_empty()))
501        {
502            return Err(ValidationError::InvalidPlan);
503        }
504        input.mandatory_keys.sort_unstable();
505        if input.mandatory_keys.windows(2).any(equal_values)
506            || has_duplicate_alpn(&input.advertised_alpn)
507        {
508            return Err(ValidationError::InvalidPlan);
509        }
510        input.connection_hints.sort_unstable();
511        input.connection_hints.dedup();
512        input.parameters.sort_unstable();
513        if input.parameters.windows(2).any(|pair| {
514            pair.first().map(ServiceParameter::key) == pair.get(1).map(ServiceParameter::key)
515        }) || input
516            .parameters
517            .first()
518            .is_some_and(|parameter| parameter.key() == 0)
519        {
520            return Err(ValidationError::InvalidPlan);
521        }
522        if input
523            .mandatory_keys
524            .iter()
525            .any(|key| !matches!(key, 1..=6) || !has_service_parameter(&input.parameters, *key))
526            || !selected_alpn_is_valid(
527                input.selected_protocol,
528                &input.advertised_alpn,
529                &input.parameters,
530            )
531            || !known_service_parameters_match(&input)?
532        {
533            return Err(ValidationError::InvalidPlan);
534        }
535        Ok(Self {
536            priority: input.priority,
537            service_target: input.service_target,
538            mandatory_keys: input.mandatory_keys,
539            advertised_alpn: input.advertised_alpn,
540            selected_protocol: input.selected_protocol,
541            effective_port: input.effective_port,
542            transport: input.transport,
543            connection_hints: input.connection_hints,
544            ech_config: input.ech_config,
545            parameters: input.parameters,
546        })
547    }
548
549    /// Selected service priority.
550    #[must_use]
551    pub const fn priority(&self) -> Option<u16> {
552        self.priority
553    }
554
555    /// Effective endpoint TargetName selected by HTTPS/SVCB ServiceMode.
556    #[must_use]
557    pub const fn service_target(&self) -> &CanonicalHost {
558        &self.service_target
559    }
560
561    /// Mandatory SVCB keys.
562    #[must_use]
563    pub fn mandatory_keys(&self) -> &[u16] {
564        &self.mandatory_keys
565    }
566
567    /// Raw advertised ALPN identifiers. Implicit scheme defaults are not
568    /// inserted into this list.
569    #[must_use]
570    pub fn advertised_alpn(&self) -> &[Vec<u8>] {
571        &self.advertised_alpn
572    }
573
574    /// Selected application protocol.
575    #[must_use]
576    pub const fn selected_protocol(&self) -> ApplicationProtocol {
577        self.selected_protocol
578    }
579
580    /// Effective service port.
581    #[must_use]
582    pub const fn effective_port(&self) -> NonZeroU16 {
583        self.effective_port
584    }
585
586    /// Effective service transport.
587    #[must_use]
588    pub const fn transport(&self) -> ServiceTransport {
589        self.transport
590    }
591
592    /// Connection-used address hints.
593    #[must_use]
594    pub fn connection_hints(&self) -> &[IpAddr] {
595        &self.connection_hints
596    }
597
598    /// Connection-used ECH configuration.
599    #[must_use]
600    pub fn ech_config(&self) -> Option<&[u8]> {
601        self.ech_config.as_deref()
602    }
603
604    /// Canonical supported SVCB parameters.
605    #[must_use]
606    pub fn parameters(&self) -> &[ServiceParameter] {
607        &self.parameters
608    }
609}
610
611fn equal_values<T: PartialEq>(pair: &[T]) -> bool {
612    pair.first() == pair.get(1)
613}
614
615fn has_duplicate_alpn(values: &[Vec<u8>]) -> bool {
616    values
617        .iter()
618        .enumerate()
619        .any(|(index, value)| values.iter().skip(index + 1).any(|other| other == value))
620}
621
622fn has_service_parameter(parameters: &[ServiceParameter], key: u16) -> bool {
623    parameters.iter().any(|parameter| parameter.key() == key)
624}
625
626fn service_parameter(parameters: &[ServiceParameter], key: u16) -> Option<&[u8]> {
627    parameters
628        .iter()
629        .find(|parameter| parameter.key() == key)
630        .map(ServiceParameter::value)
631}
632
633fn selected_alpn_is_valid(
634    selected: ApplicationProtocol,
635    advertised: &[Vec<u8>],
636    parameters: &[ServiceParameter],
637) -> bool {
638    let no_default_alpn = service_parameter(parameters, 2).is_some();
639    if selected == ApplicationProtocol::Http11 && !no_default_alpn {
640        return true;
641    }
642    advertised.iter().any(|identifier| match selected {
643        ApplicationProtocol::Http11 => identifier.as_slice() == b"http/1.1",
644        ApplicationProtocol::Http2 => identifier.as_slice() == b"h2",
645        ApplicationProtocol::Http3 => {
646            identifier.as_slice() == b"h3" || identifier.starts_with(b"h3-")
647        }
648    })
649}
650
651fn known_service_parameters_match(input: &ServiceBindingInput) -> Result<bool, ValidationError> {
652    if let Some(value) = service_parameter(&input.parameters, 1) {
653        if !alpn_wire_matches(value, &input.advertised_alpn)? {
654            return Ok(false);
655        }
656    } else if !input.advertised_alpn.is_empty() {
657        return Ok(false);
658    }
659    if service_parameter(&input.parameters, 2).is_some_and(|value| !value.is_empty()) {
660        return Ok(false);
661    }
662    if service_parameter(&input.parameters, 3)
663        .is_some_and(|value| value != input.effective_port.get().to_be_bytes())
664    {
665        return Ok(false);
666    }
667    if service_parameter(&input.parameters, 5) != input.ech_config.as_deref() {
668        return Ok(false);
669    }
670    if service_parameter(&input.parameters, 4)
671        .is_some_and(|wire| wire.is_empty() || wire.len() % 4 != 0)
672        || service_parameter(&input.parameters, 6)
673            .is_some_and(|wire| wire.is_empty() || wire.len() % 16 != 0)
674    {
675        return Ok(false);
676    }
677    Ok(input
678        .connection_hints
679        .iter()
680        .all(|hint| service_parameters_contain_hint(&input.parameters, *hint)))
681}
682
683fn alpn_wire_matches(wire: &[u8], advertised: &[Vec<u8>]) -> Result<bool, ValidationError> {
684    if wire.is_empty() {
685        return Err(ValidationError::InvalidPlan);
686    }
687    let mut offset = 0usize;
688    let mut parsed = Vec::new();
689    while offset < wire.len() {
690        let length = usize::from(*wire.get(offset).ok_or(ValidationError::InvalidPlan)?);
691        offset = offset.checked_add(1).ok_or(ValidationError::InvalidPlan)?;
692        if length == 0 {
693            return Err(ValidationError::InvalidPlan);
694        }
695        let end = offset
696            .checked_add(length)
697            .ok_or(ValidationError::InvalidPlan)?;
698        let identifier = wire.get(offset..end).ok_or(ValidationError::InvalidPlan)?;
699        parsed.push(identifier);
700        offset = end;
701    }
702    Ok(parsed.len() == advertised.len()
703        && parsed
704            .iter()
705            .zip(advertised)
706            .all(|(left, right)| *left == right.as_slice()))
707}
708
709fn service_parameters_contain_hint(parameters: &[ServiceParameter], hint: IpAddr) -> bool {
710    match hint {
711        IpAddr::V4(address) => service_parameter(parameters, 4)
712            .is_some_and(|wire| wire.chunks_exact(4).any(|chunk| chunk == address.octets())),
713        IpAddr::V6(address) => service_parameter(parameters, 6)
714            .is_some_and(|wire| wire.chunks_exact(16).any(|chunk| chunk == address.octets())),
715    }
716}
717
718/// Browser trust action included in convergence comparison.
719#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
720#[repr(u8)]
721pub enum TlsTrustPolicy {
722    /// Origin scheme is cleartext.
723    Cleartext = 1,
724    /// Secure TLSA data must be enforced.
725    Dane = 2,
726    /// ICANN TLSA is securely absent, so WebPKI is permitted.
727    WebPkiAuthenticatedAbsence = 3,
728    /// ICANN delegation is proven insecure, so WebPKI is permitted.
729    WebPkiInsecureDelegation = 4,
730    /// HNS TLSA is securely absent; the origin certificate must carry and
731    /// validate stateless HNS/DNSSEC/TLSA evidence before it is trusted.
732    StatelessDane = 5,
733}
734
735impl TlsTrustPolicy {
736    fn tag(self) -> u8 {
737        self as u8
738    }
739}
740
741/// Canonical TLSA RDATA retained for trust comparison.
742#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
743pub struct CanonicalTlsa(Vec<u8>);
744
745impl CanonicalTlsa {
746    /// Creates one bounded TLSA RDATA value.
747    ///
748    /// # Errors
749    ///
750    /// Returns [`ValidationError::InvalidPlan`] unless the record uses the
751    /// locally supported DANE-TA/DANE-EE usages, certificate/SPKI selectors,
752    /// exact/SHA-256/SHA-512 matching types, and a valid nonempty association
753    /// length. Oversized values return [`ValidationError::BoundExceeded`].
754    pub fn new(rdata: Vec<u8>) -> Result<Self, ValidationError> {
755        if rdata.len() > MAX_PARAMETER_BYTES {
756            return Err(ValidationError::BoundExceeded);
757        }
758        let Some((&usage, remainder)) = rdata.split_first() else {
759            return Err(ValidationError::InvalidPlan);
760        };
761        let Some((&selector, remainder)) = remainder.split_first() else {
762            return Err(ValidationError::InvalidPlan);
763        };
764        let Some((&matching, association)) = remainder.split_first() else {
765            return Err(ValidationError::InvalidPlan);
766        };
767        if !matches!(usage, 2 | 3)
768            || !matches!(selector, 0 | 1)
769            || !matches!((matching, association.len()), (0, 1..) | (1, 32) | (2, 64))
770        {
771            return Err(ValidationError::InvalidPlan);
772        }
773        Ok(Self(rdata))
774    }
775
776    /// Canonical TLSA wire RDATA.
777    #[must_use]
778    pub fn rdata(&self) -> &[u8] {
779        &self.0
780    }
781}
782
783/// DNSSEC state associated with authenticated ICANN DoH provenance.
784#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
785#[repr(u8)]
786pub enum IcannChainState {
787    /// DNSSEC-secure data or denial.
788    Secure = 1,
789    /// Authenticated validating resolver proved an insecure delegation.
790    ProvenInsecure = 2,
791}
792
793/// Evidence lineage retained for diagnostics and freshness, but ignored by
794/// semantic convergence.
795#[derive(Clone, Debug, Eq, Hash, PartialEq)]
796pub enum EvidenceProvenance {
797    /// HNS Urkel/header anchor.
798    Hns {
799        /// Canonical HNS network.
800        network: HnsNetwork,
801        /// Exact Urkel tree root carried by this resolution's verified proof.
802        tree_root: [u8; 32],
803        /// Exact canonical block height carried by that proof.
804        height: u32,
805    },
806    /// TLS-authenticated validating ICANN DoH resolver.
807    IcannDoh {
808        /// Validated DNSSEC chain state.
809        chain_state: IcannChainState,
810    },
811}
812
813impl EvidenceProvenance {
814    fn validate_for(&self, namespace: Namespace) -> Result<(), ValidationError> {
815        match (namespace, self) {
816            (Namespace::Hns, Self::Hns { .. }) | (Namespace::Icann, Self::IcannDoh { .. }) => {
817                Ok(())
818            }
819            _ => Err(ValidationError::InvalidEvidence),
820        }
821    }
822
823    fn hns_network(&self) -> Option<HnsNetwork> {
824        match self {
825            Self::Hns { network, .. } => Some(*network),
826            Self::IcannDoh { .. } => None,
827        }
828    }
829}
830
831/// Absolute observation and expiry bounds for validated evidence.
832///
833/// Adapters must retain these timestamps with cached proof/denial evidence.
834/// Reading a persisted TTL at a later time must never restart its lifetime.
835/// Expiry is the earliest applicable TTL/negative-TTL, DNSSEC signature
836/// expiry, HNS anchor-currentness deadline, and adapter lifecycle bound.
837#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
838pub struct Freshness {
839    observed_at_unix: u64,
840    expires_at_unix: u64,
841}
842
843impl Freshness {
844    /// Creates a non-empty freshness interval.
845    ///
846    /// # Errors
847    ///
848    /// Returns [`ValidationError::InvalidEvidence`] when expiry does not
849    /// follow observation time.
850    pub const fn new(observed_at_unix: u64, expires_at_unix: u64) -> Result<Self, ValidationError> {
851        if expires_at_unix <= observed_at_unix {
852            return Err(ValidationError::InvalidEvidence);
853        }
854        Ok(Self {
855            observed_at_unix,
856            expires_at_unix,
857        })
858    }
859
860    /// Evidence observation time.
861    #[must_use]
862    pub const fn observed_at_unix(self) -> u64 {
863        self.observed_at_unix
864    }
865
866    /// Exclusive evidence expiry time.
867    #[must_use]
868    pub const fn expires_at_unix(self) -> u64 {
869        self.expires_at_unix
870    }
871
872    /// Whether evidence is current at `now_unix`.
873    #[must_use]
874    pub const fn is_fresh_at(self, now_unix: u64) -> bool {
875        now_unix >= self.observed_at_unix && now_unix < self.expires_at_unix
876    }
877}
878
879/// Unvalidated complete origin plan normalized by
880/// [`ValidatedOriginPlan::new`].
881#[derive(Clone, Debug, Eq, PartialEq)]
882pub struct OriginPlanInput {
883    /// Root that independently produced every field.
884    pub namespace: Namespace,
885    /// Complete browser origin query this plan answers.
886    pub query: OriginQuery,
887    /// Bounded internally coherent alias chain.
888    pub alias_path: Vec<AliasStep>,
889    /// Terminal HTTPS/SVCB owner after origin CNAME and AliasMode processing.
890    pub terminal_target: CanonicalHost,
891    /// CNAME chain followed from the normalized HTTPS/SVCB ServiceMode
892    /// TargetName to the final address owner.
893    pub endpoint_alias_path: Vec<AliasStep>,
894    /// Final canonical owner whose A/AAAA data produced `endpoints`.
895    pub endpoint_target: CanonicalHost,
896    /// Usable selected-root endpoints.
897    pub endpoints: Vec<SocketAddr>,
898    /// Selected service binding.
899    pub service: ServiceBinding,
900    /// Browser TLS action.
901    pub tls_policy: TlsTrustPolicy,
902    /// Canonical effective-service TLSA data.
903    pub tlsa_records: Vec<CanonicalTlsa>,
904    /// Authenticated evidence lineage.
905    pub provenance: EvidenceProvenance,
906    /// Earliest absolute evidence freshness bound.
907    pub freshness: Freshness,
908}
909
910/// One complete, internally coherent origin plan from exactly one root.
911#[derive(Clone, Debug, Eq, PartialEq)]
912pub struct ValidatedOriginPlan {
913    namespace: Namespace,
914    query: OriginQuery,
915    alias_path: Vec<AliasStep>,
916    terminal_target: CanonicalHost,
917    endpoint_alias_path: Vec<AliasStep>,
918    endpoint_target: CanonicalHost,
919    endpoints: Vec<SocketAddr>,
920    service: ServiceBinding,
921    tls_policy: TlsTrustPolicy,
922    tlsa_records: Vec<CanonicalTlsa>,
923    provenance: EvidenceProvenance,
924    freshness: Freshness,
925}
926
927impl ValidatedOriginPlan {
928    /// Validates and canonicalizes one single-root origin plan.
929    ///
930    /// # Errors
931    ///
932    /// Returns a [`ValidationError`] for cross-root provenance, alias
933    /// discontinuity/cycles, endpoint/service mismatch, unsupported trust
934    /// combinations, duplicates outside bounds, or missing DANE TLSA data.
935    pub fn new(mut input: OriginPlanInput) -> Result<Self, ValidationError> {
936        input.provenance.validate_for(input.namespace)?;
937        if input.alias_path.len() > MAX_ALIAS_STEPS
938            || input.endpoint_alias_path.len() > MAX_ALIAS_STEPS
939            || input.endpoints.is_empty()
940            || input.endpoints.len() > MAX_ENDPOINTS
941            || input.tlsa_records.len() > MAX_TLSA_RECORDS
942        {
943            return Err(ValidationError::BoundExceeded);
944        }
945        validate_plan_aliases(&input)?;
946        input.endpoints.sort_unstable();
947        input.endpoints.dedup();
948        validate_plan_endpoints_and_service(&input)?;
949        input.tlsa_records.sort_unstable();
950        input.tlsa_records.dedup();
951        validate_plan_tls(&input)?;
952        Ok(Self {
953            namespace: input.namespace,
954            query: input.query,
955            alias_path: input.alias_path,
956            terminal_target: input.terminal_target,
957            endpoint_alias_path: input.endpoint_alias_path,
958            endpoint_target: input.endpoint_target,
959            endpoints: input.endpoints,
960            service: input.service,
961            tls_policy: input.tls_policy,
962            tlsa_records: input.tlsa_records,
963            provenance: input.provenance,
964            freshness: input.freshness,
965        })
966    }
967
968    /// Root that produced this complete plan.
969    #[must_use]
970    pub const fn namespace(&self) -> Namespace {
971        self.namespace
972    }
973
974    /// Complete browser origin query.
975    #[must_use]
976    pub const fn query(&self) -> &OriginQuery {
977        &self.query
978    }
979
980    /// Browser origin host.
981    #[must_use]
982    pub const fn origin_host(&self) -> &CanonicalHost {
983        self.query.host()
984    }
985
986    /// Retained alias path.
987    #[must_use]
988    pub fn alias_path(&self) -> &[AliasStep] {
989        &self.alias_path
990    }
991
992    /// Terminal canonical service target.
993    #[must_use]
994    pub const fn terminal_target(&self) -> &CanonicalHost {
995        &self.terminal_target
996    }
997
998    /// CNAME path from the service TargetName to the final address owner.
999    #[must_use]
1000    pub fn endpoint_alias_path(&self) -> &[AliasStep] {
1001        &self.endpoint_alias_path
1002    }
1003
1004    /// Final canonical A/AAAA owner.
1005    #[must_use]
1006    pub const fn endpoint_target(&self) -> &CanonicalHost {
1007        &self.endpoint_target
1008    }
1009
1010    /// Sorted, deduplicated usable endpoint set.
1011    #[must_use]
1012    pub fn endpoints(&self) -> &[SocketAddr] {
1013        &self.endpoints
1014    }
1015
1016    /// Selected HTTPS/SVCB service state.
1017    #[must_use]
1018    pub const fn service(&self) -> &ServiceBinding {
1019        &self.service
1020    }
1021
1022    /// Browser trust action.
1023    #[must_use]
1024    pub const fn tls_policy(&self) -> TlsTrustPolicy {
1025        self.tls_policy
1026    }
1027
1028    /// Canonical sorted TLSA RDATA.
1029    #[must_use]
1030    pub fn tlsa_records(&self) -> &[CanonicalTlsa] {
1031        &self.tlsa_records
1032    }
1033
1034    /// Authenticated evidence lineage.
1035    #[must_use]
1036    pub const fn provenance(&self) -> &EvidenceProvenance {
1037        &self.provenance
1038    }
1039
1040    /// Earliest evidence freshness bound.
1041    #[must_use]
1042    pub const fn freshness(&self) -> Freshness {
1043        self.freshness
1044    }
1045
1046    /// Connection/trust differences from another complete plan.
1047    ///
1048    /// Provenance, TTL-derived expiry, proof encoding, record order, resolver
1049    /// identity, and namespace identity do not create divergence.
1050    #[must_use]
1051    pub fn differences(&self, other: &Self) -> DivergenceMask {
1052        let mut mask = DivergenceMask::NONE;
1053        mask.set_if(Self::ORIGIN_QUERY_BIT, self.query != other.query);
1054        mask.set_if(Self::ALIAS_PATH_BIT, self.alias_path != other.alias_path);
1055        mask.set_if(
1056            Self::TERMINAL_TARGET_BIT,
1057            self.terminal_target != other.terminal_target,
1058        );
1059        mask.set_if(Self::ENDPOINTS_BIT, self.endpoints != other.endpoints);
1060        mask.set_if(
1061            Self::SERVICE_PRIORITY_BIT,
1062            self.service.priority != other.service.priority,
1063        );
1064        mask.set_if(
1065            Self::SERVICE_TARGET_BIT,
1066            self.service.service_target != other.service.service_target,
1067        );
1068        mask.set_if(
1069            Self::ENDPOINT_ALIAS_PATH_BIT,
1070            self.endpoint_alias_path != other.endpoint_alias_path,
1071        );
1072        mask.set_if(
1073            Self::ENDPOINT_TARGET_BIT,
1074            self.endpoint_target != other.endpoint_target,
1075        );
1076        mask.set_if(
1077            Self::MANDATORY_PARAMETERS_BIT,
1078            self.service.mandatory_keys != other.service.mandatory_keys,
1079        );
1080        mask.set_if(
1081            Self::ALPN_BIT,
1082            self.service.advertised_alpn != other.service.advertised_alpn,
1083        );
1084        mask.set_if(
1085            Self::APPLICATION_PROTOCOL_BIT,
1086            self.service.selected_protocol != other.service.selected_protocol,
1087        );
1088        mask.set_if(
1089            Self::EFFECTIVE_PORT_BIT,
1090            self.service.effective_port != other.service.effective_port,
1091        );
1092        mask.set_if(
1093            Self::TRANSPORT_BIT,
1094            self.service.transport != other.service.transport,
1095        );
1096        mask.set_if(
1097            Self::CONNECTION_HINTS_BIT,
1098            self.service.connection_hints != other.service.connection_hints,
1099        );
1100        mask.set_if(
1101            Self::ECH_BIT,
1102            self.service.ech_config != other.service.ech_config,
1103        );
1104        mask.set_if(
1105            Self::SERVICE_PARAMETERS_BIT,
1106            self.service.parameters != other.service.parameters,
1107        );
1108        mask.set_if(Self::TLS_POLICY_BIT, self.tls_policy != other.tls_policy);
1109        mask.set_if(Self::TLSA_BIT, self.tlsa_records != other.tlsa_records);
1110        mask
1111    }
1112
1113    /// Whether every connection- and trust-affecting field converges.
1114    #[must_use]
1115    pub fn equivalent_to(&self, other: &Self) -> bool {
1116        self.differences(other).is_empty()
1117    }
1118
1119    const ORIGIN_QUERY_BIT: u32 = 1 << 0;
1120    const ALIAS_PATH_BIT: u32 = 1 << 1;
1121    const TERMINAL_TARGET_BIT: u32 = 1 << 2;
1122    const ENDPOINTS_BIT: u32 = 1 << 3;
1123    const SERVICE_PRIORITY_BIT: u32 = 1 << 4;
1124    const SERVICE_TARGET_BIT: u32 = 1 << 5;
1125    const ENDPOINT_ALIAS_PATH_BIT: u32 = 1 << 6;
1126    const ENDPOINT_TARGET_BIT: u32 = 1 << 7;
1127    const MANDATORY_PARAMETERS_BIT: u32 = 1 << 8;
1128    const ALPN_BIT: u32 = 1 << 9;
1129    const APPLICATION_PROTOCOL_BIT: u32 = 1 << 10;
1130    const EFFECTIVE_PORT_BIT: u32 = 1 << 11;
1131    const TRANSPORT_BIT: u32 = 1 << 12;
1132    const CONNECTION_HINTS_BIT: u32 = 1 << 13;
1133    const ECH_BIT: u32 = 1 << 14;
1134    const SERVICE_PARAMETERS_BIT: u32 = 1 << 15;
1135    const TLS_POLICY_BIT: u32 = 1 << 16;
1136    const TLSA_BIT: u32 = 1 << 17;
1137}
1138
1139fn validate_plan_aliases(input: &OriginPlanInput) -> Result<(), ValidationError> {
1140    validate_alias_path(
1141        input.query.host(),
1142        &input.alias_path,
1143        &input.terminal_target,
1144    )?;
1145    if input
1146        .endpoint_alias_path
1147        .iter()
1148        .any(|alias| alias.kind() != AliasKind::Cname)
1149    {
1150        return Err(ValidationError::InvalidPlan);
1151    }
1152    validate_alias_path(
1153        input.service.service_target(),
1154        &input.endpoint_alias_path,
1155        &input.endpoint_target,
1156    )?;
1157
1158    let mut seen = vec![input.query.host()];
1159    for alias in &input.alias_path {
1160        seen.push(alias.target());
1161    }
1162    if input.service.service_target() != &input.terminal_target
1163        && seen.contains(&input.service.service_target())
1164    {
1165        return Err(ValidationError::InvalidPlan);
1166    }
1167    if !seen.contains(&input.service.service_target()) {
1168        seen.push(input.service.service_target());
1169    }
1170    for alias in &input.endpoint_alias_path {
1171        if seen.contains(&alias.target()) {
1172            return Err(ValidationError::InvalidPlan);
1173        }
1174        seen.push(alias.target());
1175    }
1176    Ok(())
1177}
1178
1179fn validate_plan_endpoints_and_service(input: &OriginPlanInput) -> Result<(), ValidationError> {
1180    let endpoint_mismatch = input.endpoints.len() > MAX_ENDPOINTS
1181        || input
1182            .endpoints
1183            .iter()
1184            .any(|endpoint| endpoint.port() != input.service.effective_port().get())
1185        || input.service.connection_hints().iter().any(|hint| {
1186            !input
1187                .endpoints
1188                .iter()
1189                .any(|endpoint| endpoint.ip() == *hint)
1190        });
1191    let query_mismatch = !input
1192        .query
1193        .supported_protocols()
1194        .supports(input.service.selected_protocol())
1195        || (!input.query.scheme().uses_tls()
1196            && (input.service.priority().is_some()
1197                || input.service.selected_protocol() != ApplicationProtocol::Http11
1198                || input.service.transport() != ServiceTransport::Tcp))
1199        || (input.query.scheme().uses_tls() && input.tls_policy == TlsTrustPolicy::Cleartext)
1200        || (!input.query.scheme().uses_tls() && input.tls_policy != TlsTrustPolicy::Cleartext)
1201        || (input.service.priority().is_none()
1202            && (input.service.service_target() != &input.terminal_target
1203                || input.service.effective_port() != input.query.origin_port()))
1204        || (input.service.priority().is_some()
1205            && !has_service_parameter(input.service.parameters(), 3)
1206            && input.service.effective_port() != input.query.origin_port());
1207    if endpoint_mismatch || query_mismatch {
1208        Err(ValidationError::InvalidPlan)
1209    } else {
1210        Ok(())
1211    }
1212}
1213
1214fn validate_plan_tls(input: &OriginPlanInput) -> Result<(), ValidationError> {
1215    let invalid_records = match input.tls_policy {
1216        TlsTrustPolicy::Dane => input.tlsa_records.is_empty(),
1217        TlsTrustPolicy::Cleartext
1218        | TlsTrustPolicy::StatelessDane
1219        | TlsTrustPolicy::WebPkiAuthenticatedAbsence
1220        | TlsTrustPolicy::WebPkiInsecureDelegation => !input.tlsa_records.is_empty(),
1221    };
1222    if invalid_records
1223        || (input.namespace == Namespace::Hns
1224            && matches!(
1225                input.tls_policy,
1226                TlsTrustPolicy::WebPkiAuthenticatedAbsence
1227                    | TlsTrustPolicy::WebPkiInsecureDelegation
1228            ))
1229    {
1230        return Err(ValidationError::InvalidPlan);
1231    }
1232    match (&input.provenance, input.tls_policy) {
1233        (
1234            EvidenceProvenance::IcannDoh {
1235                chain_state: IcannChainState::Secure,
1236            },
1237            TlsTrustPolicy::Dane | TlsTrustPolicy::WebPkiAuthenticatedAbsence,
1238        )
1239        | (
1240            EvidenceProvenance::IcannDoh {
1241                chain_state: IcannChainState::ProvenInsecure,
1242            },
1243            TlsTrustPolicy::WebPkiInsecureDelegation,
1244        )
1245        | (EvidenceProvenance::Hns { .. }, TlsTrustPolicy::Dane | TlsTrustPolicy::StatelessDane)
1246        | (_, TlsTrustPolicy::Cleartext) => Ok(()),
1247        _ => Err(ValidationError::InvalidEvidence),
1248    }
1249}
1250
1251fn validate_alias_path(
1252    origin_host: &CanonicalHost,
1253    aliases: &[AliasStep],
1254    terminal_target: &CanonicalHost,
1255) -> Result<(), ValidationError> {
1256    let mut expected_owner = origin_host;
1257    let mut seen = vec![origin_host];
1258    for alias in aliases {
1259        if alias.owner() != expected_owner || seen.contains(&alias.target()) {
1260            return Err(ValidationError::InvalidPlan);
1261        }
1262        seen.push(alias.target());
1263        expected_owner = alias.target();
1264    }
1265    if expected_owner != terminal_target {
1266        return Err(ValidationError::InvalidPlan);
1267    }
1268    Ok(())
1269}
1270
1271/// Authenticated reason that one root has no usable origin plan.
1272#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1273#[repr(u8)]
1274pub enum AbsenceKind {
1275    /// Current canonical HNS Urkel non-inclusion.
1276    HnsCurrentUrkelNonInclusion = 1,
1277    /// DNSSEC-authenticated NXDOMAIN.
1278    DnssecAuthenticatedNxDomain = 2,
1279    /// DNSSEC-authenticated name with no usable browser endpoint.
1280    DnssecAuthenticatedNoUsableEndpoint = 3,
1281    /// Insecure ICANN NXDOMAIN received from the authenticated validating DoH
1282    /// resolver.
1283    IcannInsecureNxDomain = 4,
1284    /// Insecure ICANN name with no usable endpoint, received from the
1285    /// authenticated validating DoH resolver.
1286    IcannInsecureNoUsableEndpoint = 5,
1287}
1288
1289impl AbsenceKind {
1290    fn tag(self) -> u8 {
1291        self as u8
1292    }
1293}
1294
1295/// Typed authenticated absence from one root.
1296#[derive(Clone, Debug, Eq, PartialEq)]
1297pub struct ValidatedAbsence {
1298    namespace: Namespace,
1299    query: OriginQuery,
1300    kind: AbsenceKind,
1301    provenance: EvidenceProvenance,
1302    freshness: Freshness,
1303}
1304
1305impl ValidatedAbsence {
1306    /// Creates absence evidence valid for exactly one root and complete origin
1307    /// query.
1308    ///
1309    /// # Errors
1310    ///
1311    /// Returns [`ValidationError::InvalidEvidence`] for a cross-root kind or
1312    /// provenance mismatch.
1313    pub fn new(
1314        namespace: Namespace,
1315        query: OriginQuery,
1316        kind: AbsenceKind,
1317        provenance: EvidenceProvenance,
1318        freshness: Freshness,
1319    ) -> Result<Self, ValidationError> {
1320        provenance.validate_for(namespace)?;
1321        match (namespace, kind, &provenance) {
1322            (
1323                Namespace::Hns,
1324                AbsenceKind::HnsCurrentUrkelNonInclusion
1325                | AbsenceKind::DnssecAuthenticatedNxDomain
1326                | AbsenceKind::DnssecAuthenticatedNoUsableEndpoint,
1327                EvidenceProvenance::Hns { .. },
1328            )
1329            | (
1330                Namespace::Icann,
1331                AbsenceKind::DnssecAuthenticatedNxDomain
1332                | AbsenceKind::DnssecAuthenticatedNoUsableEndpoint,
1333                EvidenceProvenance::IcannDoh {
1334                    chain_state: IcannChainState::Secure,
1335                    ..
1336                },
1337            )
1338            | (
1339                Namespace::Icann,
1340                AbsenceKind::IcannInsecureNxDomain | AbsenceKind::IcannInsecureNoUsableEndpoint,
1341                EvidenceProvenance::IcannDoh {
1342                    chain_state: IcannChainState::ProvenInsecure,
1343                    ..
1344                },
1345            ) => {}
1346            _ => return Err(ValidationError::InvalidEvidence),
1347        }
1348        Ok(Self {
1349            namespace,
1350            query,
1351            kind,
1352            provenance,
1353            freshness,
1354        })
1355    }
1356
1357    /// Root whose absence was authenticated.
1358    #[must_use]
1359    pub const fn namespace(&self) -> Namespace {
1360        self.namespace
1361    }
1362
1363    /// Complete origin query whose plan is absent.
1364    #[must_use]
1365    pub const fn query(&self) -> &OriginQuery {
1366        &self.query
1367    }
1368
1369    /// Queried complete hostname.
1370    #[must_use]
1371    pub const fn host(&self) -> &CanonicalHost {
1372        self.query.host()
1373    }
1374
1375    /// Typed absence reason.
1376    #[must_use]
1377    pub const fn kind(&self) -> AbsenceKind {
1378        self.kind
1379    }
1380
1381    /// Authenticated evidence lineage.
1382    #[must_use]
1383    pub const fn provenance(&self) -> &EvidenceProvenance {
1384        &self.provenance
1385    }
1386
1387    /// Earliest evidence freshness bound.
1388    #[must_use]
1389    pub const fn freshness(&self) -> Freshness {
1390        self.freshness
1391    }
1392}
1393
1394/// Fail-closed reason that one root did not produce presence or authenticated
1395/// absence.
1396#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1397#[repr(u8)]
1398pub enum RootFailureKind {
1399    /// Lookup deadline elapsed.
1400    Timeout = 1,
1401    /// Network transport failed.
1402    Transport = 2,
1403    /// HNS anchor is unavailable or stale.
1404    StaleHnsAnchor = 3,
1405    /// DNSSEC validation is bogus.
1406    BogusDnssec = 4,
1407    /// DNSSEC state is indeterminate.
1408    IndeterminateDnssec = 5,
1409    /// Configured validating resolver was not authenticated.
1410    UnauthenticatedResolver = 6,
1411    /// Response was malformed or internally contradictory.
1412    MalformedResponse = 7,
1413    /// Required protocol feature is unsupported.
1414    Unsupported = 8,
1415    /// Work was cancelled by a newer lifecycle generation.
1416    Cancelled = 9,
1417    /// Internal adapter failure.
1418    Internal = 10,
1419    /// Otherwise valid root evidence expired before classification completed.
1420    StaleEvidence = 11,
1421}
1422
1423/// Typed failure from one independent root lookup.
1424#[derive(Clone, Debug, Eq, PartialEq)]
1425pub struct RootFailure {
1426    namespace: Namespace,
1427    query: OriginQuery,
1428    kind: RootFailureKind,
1429    retry_after_unix: Option<u64>,
1430}
1431
1432impl RootFailure {
1433    /// Creates one root-scoped failure.
1434    #[must_use]
1435    pub const fn new(
1436        namespace: Namespace,
1437        query: OriginQuery,
1438        kind: RootFailureKind,
1439        retry_after_unix: Option<u64>,
1440    ) -> Self {
1441        Self {
1442            namespace,
1443            query,
1444            kind,
1445            retry_after_unix,
1446        }
1447    }
1448
1449    /// Failed root.
1450    #[must_use]
1451    pub const fn namespace(&self) -> Namespace {
1452        self.namespace
1453    }
1454
1455    /// Complete origin query whose lookup failed.
1456    #[must_use]
1457    pub const fn query(&self) -> &OriginQuery {
1458        &self.query
1459    }
1460
1461    /// Queried complete hostname.
1462    #[must_use]
1463    pub const fn host(&self) -> &CanonicalHost {
1464        self.query.host()
1465    }
1466
1467    /// Typed failure reason.
1468    #[must_use]
1469    pub const fn kind(&self) -> RootFailureKind {
1470        self.kind
1471    }
1472
1473    /// Optional backoff deadline. This never converts the failure into a
1474    /// negative answer.
1475    #[must_use]
1476    pub const fn retry_after_unix(&self) -> Option<u64> {
1477        self.retry_after_unix
1478    }
1479}
1480
1481/// Independent result from exactly one root.
1482#[derive(Clone, Debug, Eq, PartialEq)]
1483#[allow(
1484    clippy::large_enum_variant,
1485    reason = "lookups are short-lived validated values and keeping the public adapter API allocation-free is intentional"
1486)]
1487pub enum RootLookup {
1488    /// Complete validated plan exists.
1489    Present(ValidatedOriginPlan),
1490    /// Complete hostname is authentically absent or has no usable endpoint.
1491    Absent(ValidatedAbsence),
1492    /// Lookup is indeterminate and classification must fail closed.
1493    Failed(RootFailure),
1494}
1495
1496/// Bounded, copyable disposition of one root lookup.
1497///
1498/// This deliberately reports only whether the complete lookup was present,
1499/// authentically absent, or failed. It never retains a validated origin plan
1500/// or authenticated-absence proof.
1501#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1502#[repr(u8)]
1503pub enum RootResolutionDisposition {
1504    /// The complete origin had a validated resolution plan.
1505    Present = 1,
1506    /// The complete origin was authentically absent or had no usable endpoint.
1507    Absent = 2,
1508    /// The lookup failed and classification remained indeterminate.
1509    Failed = 3,
1510}
1511
1512/// Bounded diagnostic state retained when dual-root classification fails.
1513///
1514/// Successful lookups are reduced to a copyable disposition so an error never
1515/// duplicates a validated origin plan or authenticated-absence proof. Failed
1516/// lookups retain their typed [`RootFailure`] for safe, root-specific
1517/// diagnostics and retry policy.
1518#[derive(Clone, Debug, Eq, PartialEq)]
1519pub enum RootResolutionState {
1520    /// The complete origin had a validated resolution plan.
1521    Present,
1522    /// The complete origin was authentically absent or had no usable endpoint.
1523    Absent,
1524    /// The lookup failed and classification remained indeterminate.
1525    Failed(RootFailure),
1526}
1527
1528impl RootResolutionState {
1529    fn from_lookup(lookup: &RootLookup) -> Self {
1530        match lookup {
1531            RootLookup::Present(_) => Self::Present,
1532            RootLookup::Absent(_) => Self::Absent,
1533            RootLookup::Failed(failure) => Self::Failed(failure.clone()),
1534        }
1535    }
1536
1537    /// Returns the copyable lookup disposition.
1538    #[must_use]
1539    pub const fn disposition(&self) -> RootResolutionDisposition {
1540        match self {
1541            Self::Present => RootResolutionDisposition::Present,
1542            Self::Absent => RootResolutionDisposition::Absent,
1543            Self::Failed(_) => RootResolutionDisposition::Failed,
1544        }
1545    }
1546
1547    /// Returns the typed root failure, if this lookup failed.
1548    #[must_use]
1549    pub const fn failure(&self) -> Option<&RootFailure> {
1550        match self {
1551            Self::Failed(failure) => Some(failure),
1552            Self::Present | Self::Absent => None,
1553        }
1554    }
1555
1556    /// Reports whether this lookup failed.
1557    #[must_use]
1558    pub const fn is_failed(&self) -> bool {
1559        matches!(self, Self::Failed(_))
1560    }
1561}
1562
1563impl fmt::Display for RootResolutionState {
1564    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1565        match self {
1566            Self::Present => formatter.write_str("present"),
1567            Self::Absent => formatter.write_str("absent"),
1568            Self::Failed(failure) => write!(formatter, "failed ({:?})", failure.kind()),
1569        }
1570    }
1571}
1572
1573/// Behavior when both valid plans diverge and no pin/binding exists.
1574#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1575#[repr(u8)]
1576pub enum DefaultPrecedence {
1577    /// Required first-use ICANN default.
1578    PreferIcann = 1,
1579    /// Require an explicit pin instead of selecting either plan.
1580    FailClosed = 2,
1581}
1582
1583impl DefaultPrecedence {
1584    fn tag(self) -> u8 {
1585        self as u8
1586    }
1587}
1588
1589/// Per-origin selection inputs supplied by a browser profile.
1590#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1591pub struct SelectionPolicy {
1592    explicit_pin: Option<Namespace>,
1593    sticky_binding: Option<Namespace>,
1594    default_precedence: DefaultPrecedence,
1595    revision: u64,
1596}
1597
1598impl SelectionPolicy {
1599    /// Creates a selection policy without a pin or previous binding.
1600    #[must_use]
1601    pub const fn new(default_precedence: DefaultPrecedence, revision: u64) -> Self {
1602        Self {
1603            explicit_pin: None,
1604            sticky_binding: None,
1605            default_precedence,
1606            revision,
1607        }
1608    }
1609
1610    /// Adds the exact origin's explicit user pin.
1611    #[must_use]
1612    pub const fn with_explicit_pin(mut self, namespace: Option<Namespace>) -> Self {
1613        self.explicit_pin = namespace;
1614        self
1615    }
1616
1617    /// Adds the exact origin's last successful persistent binding.
1618    #[must_use]
1619    pub const fn with_sticky_binding(mut self, namespace: Option<Namespace>) -> Self {
1620        self.sticky_binding = namespace;
1621        self
1622    }
1623
1624    /// Explicit pin.
1625    #[must_use]
1626    pub const fn explicit_pin(self) -> Option<Namespace> {
1627        self.explicit_pin
1628    }
1629
1630    /// Last successful persistent binding.
1631    #[must_use]
1632    pub const fn sticky_binding(self) -> Option<Namespace> {
1633        self.sticky_binding
1634    }
1635
1636    /// Default divergence precedence.
1637    #[must_use]
1638    pub const fn default_precedence(self) -> DefaultPrecedence {
1639        self.default_precedence
1640    }
1641
1642    /// Profile binding/policy revision.
1643    #[must_use]
1644    pub const fn revision(self) -> u64 {
1645        self.revision
1646    }
1647}
1648
1649impl Default for SelectionPolicy {
1650    fn default() -> Self {
1651        Self::new(DefaultPrecedence::PreferIcann, 0)
1652    }
1653}
1654
1655/// Reason the selected namespace won precedence.
1656#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1657#[repr(u8)]
1658pub enum SelectionReason {
1659    /// Only one root produced a valid plan.
1660    SingleRoot = 1,
1661    /// Explicit per-origin user pin.
1662    ExplicitPin = 2,
1663    /// Persistent successful per-origin binding.
1664    StickyBinding = 3,
1665    /// First-use ICANN default.
1666    IcannDefault = 4,
1667}
1668
1669impl SelectionReason {
1670    fn tag(self) -> u8 {
1671        self as u8
1672    }
1673}
1674
1675/// Bit mask describing connection/trust differences between two valid plans.
1676#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1677pub struct DivergenceMask(u32);
1678
1679impl DivergenceMask {
1680    /// No connection/trust differences.
1681    pub const NONE: Self = Self(0);
1682    /// Complete origin query differs.
1683    pub const ORIGIN_QUERY: Self = Self(ValidatedOriginPlan::ORIGIN_QUERY_BIT);
1684    /// Alias path differs.
1685    pub const ALIAS_PATH: Self = Self(ValidatedOriginPlan::ALIAS_PATH_BIT);
1686    /// Terminal target differs.
1687    pub const TERMINAL_TARGET: Self = Self(ValidatedOriginPlan::TERMINAL_TARGET_BIT);
1688    /// Usable endpoint set differs.
1689    pub const ENDPOINTS: Self = Self(ValidatedOriginPlan::ENDPOINTS_BIT);
1690    /// HTTPS/SVCB priority differs.
1691    pub const SERVICE_PRIORITY: Self = Self(ValidatedOriginPlan::SERVICE_PRIORITY_BIT);
1692    /// Effective HTTPS/SVCB ServiceMode target differs.
1693    pub const SERVICE_TARGET: Self = Self(ValidatedOriginPlan::SERVICE_TARGET_BIT);
1694    /// CNAME path from service target to address owner differs.
1695    pub const ENDPOINT_ALIAS_PATH: Self = Self(ValidatedOriginPlan::ENDPOINT_ALIAS_PATH_BIT);
1696    /// Final A/AAAA owner differs.
1697    pub const ENDPOINT_TARGET: Self = Self(ValidatedOriginPlan::ENDPOINT_TARGET_BIT);
1698    /// Mandatory SVCB keys differ.
1699    pub const MANDATORY_PARAMETERS: Self = Self(ValidatedOriginPlan::MANDATORY_PARAMETERS_BIT);
1700    /// Advertised ALPN differs.
1701    pub const ALPN: Self = Self(ValidatedOriginPlan::ALPN_BIT);
1702    /// Selected application protocol differs.
1703    pub const APPLICATION_PROTOCOL: Self = Self(ValidatedOriginPlan::APPLICATION_PROTOCOL_BIT);
1704    /// Effective port differs.
1705    pub const EFFECTIVE_PORT: Self = Self(ValidatedOriginPlan::EFFECTIVE_PORT_BIT);
1706    /// Effective transport differs.
1707    pub const TRANSPORT: Self = Self(ValidatedOriginPlan::TRANSPORT_BIT);
1708    /// Connection-used address hints differ.
1709    pub const CONNECTION_HINTS: Self = Self(ValidatedOriginPlan::CONNECTION_HINTS_BIT);
1710    /// ECH configuration differs.
1711    pub const ECH: Self = Self(ValidatedOriginPlan::ECH_BIT);
1712    /// Other supported SVCB parameters differ.
1713    pub const SERVICE_PARAMETERS: Self = Self(ValidatedOriginPlan::SERVICE_PARAMETERS_BIT);
1714    /// Browser trust action differs.
1715    pub const TLS_POLICY: Self = Self(ValidatedOriginPlan::TLS_POLICY_BIT);
1716    /// Canonical TLSA RDATA differs.
1717    pub const TLSA: Self = Self(ValidatedOriginPlan::TLSA_BIT);
1718
1719    /// Raw stable mask bits.
1720    #[must_use]
1721    pub const fn bits(self) -> u32 {
1722        self.0
1723    }
1724
1725    /// Whether no difference is present.
1726    #[must_use]
1727    pub const fn is_empty(self) -> bool {
1728        self.0 == 0
1729    }
1730
1731    /// Whether every bit in `other` is present.
1732    #[must_use]
1733    pub const fn contains(self, other: Self) -> bool {
1734        self.0 & other.0 == other.0
1735    }
1736
1737    fn set_if(&mut self, bit: u32, condition: bool) {
1738        if condition {
1739            self.0 |= bit;
1740        }
1741    }
1742}
1743
1744/// Authoritative five-way dual-root outcome.
1745#[derive(Clone, Debug, Eq, PartialEq)]
1746#[allow(
1747    clippy::large_enum_variant,
1748    reason = "outcomes retain complete evidence from both roots for diagnostics and freshness"
1749)]
1750pub enum NamespaceOutcome {
1751    /// Only HNS has a valid plan.
1752    HnsOnly {
1753        /// Selected HNS plan.
1754        plan: ValidatedOriginPlan,
1755        /// Authenticated ICANN absence.
1756        icann_absence: ValidatedAbsence,
1757        /// Pin, binding, or single-root reason for the selection.
1758        selection_reason: SelectionReason,
1759    },
1760    /// Only ICANN has a valid plan.
1761    IcannOnly {
1762        /// Selected ICANN plan.
1763        plan: ValidatedOriginPlan,
1764        /// Authenticated HNS absence.
1765        hns_absence: ValidatedAbsence,
1766        /// Pin, binding, or single-root reason for the selection.
1767        selection_reason: SelectionReason,
1768    },
1769    /// Both roots produce the same connection/trust behavior.
1770    BothConvergent {
1771        /// Namespace whose plan is used.
1772        selected: Namespace,
1773        /// Precedence source used for the selected root identity.
1774        selection_reason: SelectionReason,
1775        /// Complete HNS plan.
1776        hns: ValidatedOriginPlan,
1777        /// Complete ICANN plan.
1778        icann: ValidatedOriginPlan,
1779    },
1780    /// Both roots produce different valid plans.
1781    BothDivergent {
1782        /// Namespace selected by explicit precedence.
1783        selected: Namespace,
1784        /// Precedence source.
1785        selection_reason: SelectionReason,
1786        /// Complete HNS plan.
1787        hns: ValidatedOriginPlan,
1788        /// Complete ICANN plan.
1789        icann: ValidatedOriginPlan,
1790        /// Exact semantic differences.
1791        differences: DivergenceMask,
1792    },
1793    /// Both roots authentically lack a usable origin.
1794    Neither {
1795        /// Authenticated HNS absence.
1796        hns: ValidatedAbsence,
1797        /// Authenticated ICANN absence.
1798        icann: ValidatedAbsence,
1799    },
1800}
1801
1802/// Stable outcome discriminator for status and cache schemas.
1803#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1804#[repr(u8)]
1805pub enum OutcomeKind {
1806    /// HNS only.
1807    HnsOnly = 1,
1808    /// ICANN only.
1809    IcannOnly = 2,
1810    /// Both roots converge.
1811    BothConvergent = 3,
1812    /// Both roots diverge.
1813    BothDivergent = 4,
1814    /// Neither root has a usable origin.
1815    Neither = 5,
1816}
1817
1818impl NamespaceOutcome {
1819    /// Stable five-way outcome kind.
1820    #[must_use]
1821    pub const fn kind(&self) -> OutcomeKind {
1822        match self {
1823            Self::HnsOnly { .. } => OutcomeKind::HnsOnly,
1824            Self::IcannOnly { .. } => OutcomeKind::IcannOnly,
1825            Self::BothConvergent { .. } => OutcomeKind::BothConvergent,
1826            Self::BothDivergent { .. } => OutcomeKind::BothDivergent,
1827            Self::Neither { .. } => OutcomeKind::Neither,
1828        }
1829    }
1830
1831    /// Selected namespace, or `None` for `Neither`.
1832    #[must_use]
1833    pub const fn selected_namespace(&self) -> Option<Namespace> {
1834        match self {
1835            Self::HnsOnly { .. } => Some(Namespace::Hns),
1836            Self::IcannOnly { .. } => Some(Namespace::Icann),
1837            Self::BothConvergent { selected, .. } | Self::BothDivergent { selected, .. } => {
1838                Some(*selected)
1839            }
1840            Self::Neither { .. } => None,
1841        }
1842    }
1843
1844    /// Selection source, or `None` for `Neither`.
1845    #[must_use]
1846    pub const fn selection_reason(&self) -> Option<SelectionReason> {
1847        match self {
1848            Self::HnsOnly {
1849                selection_reason, ..
1850            }
1851            | Self::IcannOnly {
1852                selection_reason, ..
1853            }
1854            | Self::BothConvergent {
1855                selection_reason, ..
1856            }
1857            | Self::BothDivergent {
1858                selection_reason, ..
1859            } => Some(*selection_reason),
1860            Self::Neither { .. } => None,
1861        }
1862    }
1863
1864    /// Selected complete plan, or `None` for `Neither`.
1865    #[must_use]
1866    pub const fn selected_plan(&self) -> Option<&ValidatedOriginPlan> {
1867        match self {
1868            Self::HnsOnly { plan, .. } | Self::IcannOnly { plan, .. } => Some(plan),
1869            Self::BothConvergent {
1870                selected,
1871                hns,
1872                icann,
1873                ..
1874            }
1875            | Self::BothDivergent {
1876                selected,
1877                hns,
1878                icann,
1879                ..
1880            } => match selected {
1881                Namespace::Hns => Some(hns),
1882                Namespace::Icann => Some(icann),
1883            },
1884            Self::Neither { .. } => None,
1885        }
1886    }
1887
1888    /// Divergence mask for `BothDivergent`.
1889    #[must_use]
1890    pub const fn divergence(&self) -> Option<DivergenceMask> {
1891        match self {
1892            Self::BothDivergent { differences, .. } => Some(*differences),
1893            _ => None,
1894        }
1895    }
1896
1897    fn hns_network(&self) -> HnsNetwork {
1898        let provenance = match self {
1899            Self::HnsOnly { plan, .. }
1900            | Self::BothConvergent { hns: plan, .. }
1901            | Self::BothDivergent { hns: plan, .. } => plan.provenance(),
1902            Self::IcannOnly { hns_absence, .. } => hns_absence.provenance(),
1903            Self::Neither { hns, .. } => hns.provenance(),
1904        };
1905        provenance
1906            .hns_network()
1907            .unwrap_or_else(|| unreachable!("validated HNS evidence always carries an HNS network"))
1908    }
1909
1910    /// Earliest expiry across both roots' retained decision evidence.
1911    #[must_use]
1912    pub fn expires_at_unix(&self) -> u64 {
1913        match self {
1914            Self::HnsOnly {
1915                plan,
1916                icann_absence,
1917                ..
1918            } => plan
1919                .freshness()
1920                .expires_at_unix()
1921                .min(icann_absence.freshness().expires_at_unix()),
1922            Self::IcannOnly {
1923                plan, hns_absence, ..
1924            } => plan
1925                .freshness()
1926                .expires_at_unix()
1927                .min(hns_absence.freshness().expires_at_unix()),
1928            Self::BothConvergent { hns, icann, .. } | Self::BothDivergent { hns, icann, .. } => hns
1929                .freshness()
1930                .expires_at_unix()
1931                .min(icann.freshness().expires_at_unix()),
1932            Self::Neither { hns, icann } => hns
1933                .freshness()
1934                .expires_at_unix()
1935                .min(icann.freshness().expires_at_unix()),
1936        }
1937    }
1938
1939    /// Whether every retained evidence item is fresh at `now_unix`.
1940    #[must_use]
1941    pub fn is_fresh_at(&self, now_unix: u64) -> bool {
1942        now_unix < self.expires_at_unix()
1943            && match self {
1944                Self::HnsOnly {
1945                    plan,
1946                    icann_absence,
1947                    ..
1948                } => {
1949                    plan.freshness().is_fresh_at(now_unix)
1950                        && icann_absence.freshness().is_fresh_at(now_unix)
1951                }
1952                Self::IcannOnly {
1953                    plan, hns_absence, ..
1954                } => {
1955                    plan.freshness().is_fresh_at(now_unix)
1956                        && hns_absence.freshness().is_fresh_at(now_unix)
1957                }
1958                Self::BothConvergent { hns, icann, .. }
1959                | Self::BothDivergent { hns, icann, .. } => {
1960                    hns.freshness().is_fresh_at(now_unix) && icann.freshness().is_fresh_at(now_unix)
1961                }
1962                Self::Neither { hns, icann } => {
1963                    hns.freshness().is_fresh_at(now_unix) && icann.freshness().is_fresh_at(now_unix)
1964                }
1965            }
1966    }
1967}
1968
1969/// Query- and policy-bound authoritative namespace decision.
1970#[derive(Clone, Debug, Eq, PartialEq)]
1971pub struct NamespaceDecision {
1972    query: OriginQuery,
1973    policy: SelectionPolicy,
1974    outcome: NamespaceOutcome,
1975}
1976
1977impl NamespaceDecision {
1978    /// Exact origin query classified through both roots.
1979    #[must_use]
1980    pub const fn query(&self) -> &OriginQuery {
1981        &self.query
1982    }
1983
1984    /// Exact policy snapshot that produced this decision.
1985    #[must_use]
1986    pub const fn policy(&self) -> SelectionPolicy {
1987        self.policy
1988    }
1989
1990    /// Five-way authoritative outcome.
1991    #[must_use]
1992    pub const fn outcome(&self) -> &NamespaceOutcome {
1993        &self.outcome
1994    }
1995
1996    /// Stable five-way outcome kind.
1997    #[must_use]
1998    pub const fn kind(&self) -> OutcomeKind {
1999        self.outcome.kind()
2000    }
2001
2002    /// Selected namespace, or `None` for `Neither`.
2003    #[must_use]
2004    pub const fn selected_namespace(&self) -> Option<Namespace> {
2005        self.outcome.selected_namespace()
2006    }
2007
2008    /// Selection source, or `None` for `Neither`.
2009    #[must_use]
2010    pub const fn selection_reason(&self) -> Option<SelectionReason> {
2011        self.outcome.selection_reason()
2012    }
2013
2014    /// Selected complete single-root plan, or `None` for `Neither`.
2015    #[must_use]
2016    pub const fn selected_plan(&self) -> Option<&ValidatedOriginPlan> {
2017        self.outcome.selected_plan()
2018    }
2019
2020    /// Divergence mask for a divergent outcome.
2021    #[must_use]
2022    pub const fn divergence(&self) -> Option<DivergenceMask> {
2023        self.outcome.divergence()
2024    }
2025
2026    /// Canonical HNS network carried by the exact retained lookup evidence.
2027    #[must_use]
2028    pub fn hns_network(&self) -> HnsNetwork {
2029        self.outcome.hns_network()
2030    }
2031
2032    /// Earliest absolute expiry across both roots.
2033    #[must_use]
2034    pub fn expires_at_unix(&self) -> u64 {
2035        self.outcome.expires_at_unix()
2036    }
2037
2038    /// Whether every retained evidence item is fresh.
2039    #[must_use]
2040    pub fn is_fresh_at(&self, now_unix: u64) -> bool {
2041        self.outcome.is_fresh_at(now_unix)
2042    }
2043}
2044
2045/// Applies the five-way classification and explicit divergence precedence.
2046///
2047/// Both root lookups must already be complete and independently validated.
2048/// A failure from either root makes the result indeterminate and returns
2049/// [`ClassificationError::RootFailed`]. Stale evidence is rejected at this
2050/// boundary rather than being left for callers to remember to check.
2051///
2052/// # Errors
2053///
2054/// Returns a [`ClassificationError`] for failed roots, mismatched query
2055/// evidence, stale evidence, or fail-closed divergence policy without a
2056/// pin/binding.
2057pub fn decide_namespace(
2058    query: &OriginQuery,
2059    hns: RootLookup,
2060    icann: RootLookup,
2061    policy: SelectionPolicy,
2062    now_unix: u64,
2063) -> Result<NamespaceDecision, ClassificationError> {
2064    validate_lookup(query, Namespace::Hns, &hns, now_unix)?;
2065    validate_lookup(query, Namespace::Icann, &icann, now_unix)?;
2066
2067    if matches!(&hns, RootLookup::Failed(_)) || matches!(&icann, RootLookup::Failed(_)) {
2068        return Err(ClassificationError::RootFailed {
2069            hns: RootResolutionState::from_lookup(&hns),
2070            icann: RootResolutionState::from_lookup(&icann),
2071        });
2072    }
2073
2074    let outcome = match (hns, icann) {
2075        (RootLookup::Present(plan), RootLookup::Absent(icann_absence)) => {
2076            let selection_reason = select_single_namespace(policy, Namespace::Hns)?;
2077            NamespaceOutcome::HnsOnly {
2078                plan,
2079                icann_absence,
2080                selection_reason,
2081            }
2082        }
2083        (RootLookup::Absent(hns_absence), RootLookup::Present(plan)) => {
2084            let selection_reason = select_single_namespace(policy, Namespace::Icann)?;
2085            NamespaceOutcome::IcannOnly {
2086                plan,
2087                hns_absence,
2088                selection_reason,
2089            }
2090        }
2091        (RootLookup::Absent(hns), RootLookup::Absent(icann)) => {
2092            NamespaceOutcome::Neither { hns, icann }
2093        }
2094        (RootLookup::Present(hns), RootLookup::Present(icann)) => {
2095            let differences = hns.differences(&icann);
2096            let (selected, selection_reason) = select_namespace(policy, differences.is_empty())?;
2097            if differences.is_empty() {
2098                NamespaceOutcome::BothConvergent {
2099                    selected,
2100                    selection_reason,
2101                    hns,
2102                    icann,
2103                }
2104            } else {
2105                NamespaceOutcome::BothDivergent {
2106                    selected,
2107                    selection_reason,
2108                    hns,
2109                    icann,
2110                    differences,
2111                }
2112            }
2113        }
2114        (RootLookup::Failed(_), _) | (_, RootLookup::Failed(_)) => {
2115            unreachable!("root failures returned before outcome matching")
2116        }
2117    };
2118    Ok(NamespaceDecision {
2119        query: query.clone(),
2120        policy,
2121        outcome,
2122    })
2123}
2124
2125fn validate_lookup(
2126    query: &OriginQuery,
2127    expected_namespace: Namespace,
2128    lookup: &RootLookup,
2129    now_unix: u64,
2130) -> Result<(), ClassificationError> {
2131    let (actual_namespace, evidence_query, fresh) = match lookup {
2132        RootLookup::Present(plan) => (
2133            plan.namespace(),
2134            plan.query(),
2135            Some(plan.freshness().is_fresh_at(now_unix)),
2136        ),
2137        RootLookup::Absent(absence) => (
2138            absence.namespace(),
2139            absence.query(),
2140            Some(absence.freshness().is_fresh_at(now_unix)),
2141        ),
2142        RootLookup::Failed(failure) => (failure.namespace(), failure.query(), None),
2143    };
2144    if actual_namespace != expected_namespace {
2145        return Err(ClassificationError::RootPositionMismatch {
2146            expected: expected_namespace,
2147            actual: actual_namespace,
2148        });
2149    }
2150    if evidence_query != query {
2151        return Err(ClassificationError::QueryMismatch {
2152            namespace: expected_namespace,
2153        });
2154    }
2155    if fresh == Some(false) {
2156        return Err(ClassificationError::StaleEvidence {
2157            namespace: expected_namespace,
2158        });
2159    }
2160    Ok(())
2161}
2162
2163fn select_namespace(
2164    policy: SelectionPolicy,
2165    convergent: bool,
2166) -> Result<(Namespace, SelectionReason), ClassificationError> {
2167    if let Some(namespace) = policy.explicit_pin() {
2168        return Ok((namespace, SelectionReason::ExplicitPin));
2169    }
2170    if let Some(namespace) = policy.sticky_binding() {
2171        return Ok((namespace, SelectionReason::StickyBinding));
2172    }
2173    match policy.default_precedence() {
2174        DefaultPrecedence::PreferIcann => Ok((Namespace::Icann, SelectionReason::IcannDefault)),
2175        DefaultPrecedence::FailClosed if convergent => {
2176            Ok((Namespace::Icann, SelectionReason::IcannDefault))
2177        }
2178        DefaultPrecedence::FailClosed => Err(ClassificationError::DivergenceRequiresSelection),
2179    }
2180}
2181
2182fn select_single_namespace(
2183    policy: SelectionPolicy,
2184    available: Namespace,
2185) -> Result<SelectionReason, ClassificationError> {
2186    if let Some(namespace) = policy.explicit_pin() {
2187        return if namespace == available {
2188            Ok(SelectionReason::ExplicitPin)
2189        } else {
2190            Err(ClassificationError::SelectedNamespaceUnavailable {
2191                namespace,
2192                selection_reason: SelectionReason::ExplicitPin,
2193            })
2194        };
2195    }
2196    if let Some(namespace) = policy.sticky_binding() {
2197        return if namespace == available {
2198            Ok(SelectionReason::StickyBinding)
2199        } else {
2200            Err(ClassificationError::SelectedNamespaceUnavailable {
2201                namespace,
2202                selection_reason: SelectionReason::StickyBinding,
2203            })
2204        };
2205    }
2206    Ok(SelectionReason::SingleRoot)
2207}
2208
2209/// Stable SHA-256 decision identity used to partition transport state.
2210#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
2211pub struct DecisionFingerprint([u8; 32]);
2212
2213impl DecisionFingerprint {
2214    /// Raw SHA-256 bytes.
2215    #[must_use]
2216    pub const fn as_bytes(&self) -> &[u8; 32] {
2217        &self.0
2218    }
2219
2220    /// Lower-case hexadecimal representation.
2221    #[must_use]
2222    #[allow(
2223        clippy::indexing_slicing,
2224        reason = "both hexadecimal table indices are masked to four bits"
2225    )]
2226    pub fn to_hex(self) -> String {
2227        const HEX: &[u8; 16] = b"0123456789abcdef";
2228        let mut output = String::with_capacity(64);
2229        for byte in self.0 {
2230            output.push(char::from(HEX[usize::from(byte >> 4)]));
2231            output.push(char::from(HEX[usize::from(byte & 0x0f)]));
2232        }
2233        output
2234    }
2235}
2236
2237impl fmt::Debug for DecisionFingerprint {
2238    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2239        formatter
2240            .debug_tuple("DecisionFingerprint")
2241            .field(&self.to_hex())
2242            .finish()
2243    }
2244}
2245
2246impl fmt::Display for DecisionFingerprint {
2247    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2248        formatter.write_str(&self.to_hex())
2249    }
2250}
2251
2252/// Computes the normalized query-, policy-, and outcome-bound decision
2253/// identity.
2254///
2255/// Freshness timestamps, TTL values, and RRSIG/proof bytes are intentionally
2256/// excluded. Resolver configuration belongs to [`DecisionCacheKey`]. The
2257/// exact policy snapshot and selected namespace are included so a pin/binding
2258/// change cannot reuse old transport state.
2259#[must_use]
2260pub fn decision_fingerprint(decision: &NamespaceDecision) -> DecisionFingerprint {
2261    let mut input = FingerprintInput::new(b"hns-namespace-decision-v1");
2262    input.u16(COMPARISON_SCHEMA_VERSION);
2263    input.query(decision.query());
2264    let policy = decision.policy();
2265    input.u64(policy.revision());
2266    input.option_namespace(policy.explicit_pin());
2267    input.option_namespace(policy.sticky_binding());
2268    input.u8(policy.default_precedence().tag());
2269    let outcome = decision.outcome();
2270    input.u8(outcome.kind() as u8);
2271    input.option_namespace(outcome.selected_namespace());
2272    input.option_selection_reason(outcome.selection_reason());
2273    match outcome {
2274        NamespaceOutcome::HnsOnly {
2275            plan,
2276            icann_absence,
2277            ..
2278        } => {
2279            input.plan(plan);
2280            input.absence(icann_absence);
2281        }
2282        NamespaceOutcome::IcannOnly {
2283            plan, hns_absence, ..
2284        } => {
2285            input.plan(plan);
2286            input.absence(hns_absence);
2287        }
2288        NamespaceOutcome::BothConvergent { hns, icann, .. } => {
2289            input.plan(hns);
2290            input.plan(icann);
2291        }
2292        NamespaceOutcome::BothDivergent {
2293            hns,
2294            icann,
2295            differences,
2296            ..
2297        } => {
2298            input.plan(hns);
2299            input.plan(icann);
2300            input.u32(differences.bits());
2301        }
2302        NamespaceOutcome::Neither { hns, icann } => {
2303            input.absence(hns);
2304            input.absence(icann);
2305        }
2306    }
2307    DecisionFingerprint(sha256(&input.finish()))
2308}
2309
2310/// Complete cache-key input for one dual-root decision.
2311#[derive(Clone, Debug, Eq, Hash, PartialEq)]
2312pub struct DecisionCacheKey {
2313    decision_fingerprint: DecisionFingerprint,
2314    query: OriginQuery,
2315    hns_network: HnsNetwork,
2316    resolver_configuration: [u8; 32],
2317    trust_anchor_generation: u64,
2318    binding_revision: u64,
2319    comparison_schema_version: u16,
2320}
2321
2322impl DecisionCacheKey {
2323    /// Creates a cache key derived from the actual decision and bound to all
2324    /// external authority generations.
2325    #[must_use]
2326    pub fn new(
2327        decision: &NamespaceDecision,
2328        resolver_configuration: [u8; 32],
2329        trust_anchor_generation: u64,
2330    ) -> Self {
2331        Self {
2332            decision_fingerprint: decision_fingerprint(decision),
2333            query: decision.query().clone(),
2334            hns_network: decision.hns_network(),
2335            resolver_configuration,
2336            trust_anchor_generation,
2337            binding_revision: decision.policy().revision(),
2338            comparison_schema_version: COMPARISON_SCHEMA_VERSION,
2339        }
2340    }
2341
2342    /// Query-, policy-, selected-root-, and plan-bound cached decision.
2343    #[must_use]
2344    pub const fn decision_fingerprint(&self) -> DecisionFingerprint {
2345        self.decision_fingerprint
2346    }
2347
2348    /// Canonical origin query.
2349    #[must_use]
2350    pub const fn query(&self) -> &OriginQuery {
2351        &self.query
2352    }
2353
2354    /// Canonical HNS network partition.
2355    #[must_use]
2356    pub const fn hns_network(&self) -> HnsNetwork {
2357        self.hns_network
2358    }
2359
2360    /// Stable SHA-256 identity for cache indexing.
2361    #[must_use]
2362    pub fn fingerprint(&self) -> DecisionFingerprint {
2363        let mut input = FingerprintInput::new(b"hns-namespace-cache-key-v1");
2364        input.u16(self.comparison_schema_version);
2365        input.bytes(self.decision_fingerprint.as_bytes());
2366        input.query(&self.query);
2367        input.u8(self.hns_network.tag());
2368        input.bytes(&self.resolver_configuration);
2369        input.u64(self.trust_anchor_generation);
2370        input.u64(self.binding_revision);
2371        DecisionFingerprint(sha256(&input.finish()))
2372    }
2373}
2374
2375/// Input/plan validation failure.
2376#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2377#[non_exhaustive]
2378pub enum ValidationError {
2379    /// Host is not one canonical DNS name.
2380    InvalidHost,
2381    /// Origin query has no usable protocol capability.
2382    InvalidQuery,
2383    /// A bounded field exceeds the contract limit.
2384    BoundExceeded,
2385    /// Origin plan is internally inconsistent.
2386    InvalidPlan,
2387    /// Evidence provenance, absence kind, or freshness is invalid.
2388    InvalidEvidence,
2389}
2390
2391impl fmt::Display for ValidationError {
2392    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2393        formatter.write_str(match self {
2394            Self::InvalidHost => "invalid canonical DNS host",
2395            Self::InvalidQuery => "invalid origin query",
2396            Self::BoundExceeded => "namespace contract bound exceeded",
2397            Self::InvalidPlan => "origin plan is internally inconsistent",
2398            Self::InvalidEvidence => "root evidence is invalid",
2399        })
2400    }
2401}
2402
2403impl std::error::Error for ValidationError {}
2404
2405/// Fail-closed classification failure.
2406#[derive(Clone, Debug, Eq, PartialEq)]
2407#[non_exhaustive]
2408pub enum ClassificationError {
2409    /// One or both root lookups failed. A nonfailed root's successful
2410    /// disposition is retained without its validated plan or absence proof.
2411    RootFailed {
2412        /// Bounded HNS lookup state.
2413        hns: RootResolutionState,
2414        /// Bounded ICANN lookup state.
2415        icann: RootResolutionState,
2416    },
2417    /// Evidence was supplied in the wrong root position.
2418    RootPositionMismatch {
2419        /// Root required by the input slot.
2420        expected: Namespace,
2421        /// Root carried by the evidence.
2422        actual: Namespace,
2423    },
2424    /// Evidence belongs to another scheme, hostname, port, or protocol
2425    /// capability set.
2426    QueryMismatch {
2427        /// Root whose complete origin query mismatched.
2428        namespace: Namespace,
2429    },
2430    /// Presence or authenticated-absence evidence is outside its validity
2431    /// interval.
2432    StaleEvidence {
2433        /// Root whose evidence is stale or not yet valid.
2434        namespace: Namespace,
2435    },
2436    /// A pin or persistent binding names a root that is authentically absent.
2437    /// Callers must perform an explicit state-isolated namespace switch rather
2438    /// than silently selecting the other root.
2439    SelectedNamespaceUnavailable {
2440        /// Bound namespace that is unavailable.
2441        namespace: Namespace,
2442        /// Whether the unavailable selection came from a pin or binding.
2443        selection_reason: SelectionReason,
2444    },
2445    /// Both roots differ and policy requires an explicit selection.
2446    DivergenceRequiresSelection,
2447}
2448
2449impl fmt::Display for ClassificationError {
2450    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2451        match self {
2452            Self::RootFailed { hns, icann } => write!(
2453                formatter,
2454                "dual-root classification is indeterminate (HNS: {hns}, ICANN: {icann})"
2455            ),
2456            Self::RootPositionMismatch { expected, actual } => write!(
2457                formatter,
2458                "root evidence position mismatch: expected {expected:?}, got {actual:?}"
2459            ),
2460            Self::QueryMismatch { namespace } => {
2461                write!(
2462                    formatter,
2463                    "{namespace:?} evidence belongs to another origin query"
2464                )
2465            }
2466            Self::StaleEvidence { namespace } => {
2467                write!(
2468                    formatter,
2469                    "{namespace:?} evidence is outside its validity interval"
2470                )
2471            }
2472            Self::SelectedNamespaceUnavailable {
2473                namespace,
2474                selection_reason,
2475            } => write!(
2476                formatter,
2477                "{selection_reason:?} namespace {namespace:?} is authentically absent"
2478            ),
2479            Self::DivergenceRequiresSelection => {
2480                formatter.write_str("divergent roots require an explicit namespace selection")
2481            }
2482        }
2483    }
2484}
2485
2486impl std::error::Error for ClassificationError {}
2487
2488struct FingerprintInput {
2489    bytes: Vec<u8>,
2490}
2491
2492impl FingerprintInput {
2493    fn new(domain: &[u8]) -> Self {
2494        let mut value = Self { bytes: Vec::new() };
2495        value.bytes(domain);
2496        value
2497    }
2498
2499    fn finish(self) -> Vec<u8> {
2500        self.bytes
2501    }
2502
2503    fn u8(&mut self, value: u8) {
2504        self.bytes.push(value);
2505    }
2506
2507    fn u16(&mut self, value: u16) {
2508        self.bytes.extend_from_slice(&value.to_be_bytes());
2509    }
2510
2511    fn u32(&mut self, value: u32) {
2512        self.bytes.extend_from_slice(&value.to_be_bytes());
2513    }
2514
2515    fn u64(&mut self, value: u64) {
2516        self.bytes.extend_from_slice(&value.to_be_bytes());
2517    }
2518
2519    fn bytes(&mut self, value: &[u8]) {
2520        self.u64(u64::try_from(value.len()).unwrap_or(u64::MAX));
2521        self.bytes.extend_from_slice(value);
2522    }
2523
2524    fn string(&mut self, value: &str) {
2525        self.bytes(value.as_bytes());
2526    }
2527
2528    fn option_namespace(&mut self, value: Option<Namespace>) {
2529        self.u8(value.map_or(0, Namespace::tag));
2530    }
2531
2532    fn option_selection_reason(&mut self, value: Option<SelectionReason>) {
2533        self.u8(value.map_or(0, SelectionReason::tag));
2534    }
2535
2536    fn query(&mut self, query: &OriginQuery) {
2537        self.string(query.host().as_str());
2538        self.u8(query.scheme().tag());
2539        self.u16(query.origin_port().get());
2540        self.u8(query.supported_protocols().flags());
2541    }
2542
2543    fn absence(&mut self, absence: &ValidatedAbsence) {
2544        self.u8(absence.namespace().tag());
2545        self.provenance_authority(absence.provenance());
2546        self.query(absence.query());
2547        self.u8(absence.kind().tag());
2548    }
2549
2550    fn plan(&mut self, plan: &ValidatedOriginPlan) {
2551        self.u8(plan.namespace().tag());
2552        self.provenance_authority(plan.provenance());
2553        self.query(plan.query());
2554        self.u64(u64::try_from(plan.alias_path().len()).unwrap_or(u64::MAX));
2555        for alias in plan.alias_path() {
2556            self.u8(alias.kind().tag());
2557            self.string(alias.owner().as_str());
2558            self.string(alias.target().as_str());
2559        }
2560        self.string(plan.terminal_target().as_str());
2561        self.u64(u64::try_from(plan.endpoint_alias_path().len()).unwrap_or(u64::MAX));
2562        for alias in plan.endpoint_alias_path() {
2563            self.u8(alias.kind().tag());
2564            self.string(alias.owner().as_str());
2565            self.string(alias.target().as_str());
2566        }
2567        self.string(plan.endpoint_target().as_str());
2568        self.u64(u64::try_from(plan.endpoints().len()).unwrap_or(u64::MAX));
2569        for endpoint in plan.endpoints() {
2570            self.socket_addr(*endpoint);
2571        }
2572        self.service(plan.service());
2573        self.u8(plan.tls_policy().tag());
2574        self.u64(u64::try_from(plan.tlsa_records().len()).unwrap_or(u64::MAX));
2575        for record in plan.tlsa_records() {
2576            self.bytes(record.rdata());
2577        }
2578    }
2579
2580    fn provenance_authority(&mut self, provenance: &EvidenceProvenance) {
2581        match provenance {
2582            EvidenceProvenance::Hns { network, .. } => {
2583                self.u8(1);
2584                self.u8(network.tag());
2585            }
2586            EvidenceProvenance::IcannDoh { .. } => self.u8(2),
2587        }
2588    }
2589
2590    fn socket_addr(&mut self, endpoint: SocketAddr) {
2591        match endpoint.ip() {
2592            IpAddr::V4(address) => {
2593                self.u8(4);
2594                self.bytes(&address.octets());
2595            }
2596            IpAddr::V6(address) => {
2597                self.u8(6);
2598                self.bytes(&address.octets());
2599            }
2600        }
2601        self.u16(endpoint.port());
2602    }
2603
2604    fn service(&mut self, service: &ServiceBinding) {
2605        match service.priority() {
2606            Some(priority) => {
2607                self.u8(1);
2608                self.u16(priority);
2609            }
2610            None => self.u8(0),
2611        }
2612        self.string(service.service_target().as_str());
2613        self.u64(u64::try_from(service.mandatory_keys().len()).unwrap_or(u64::MAX));
2614        for key in service.mandatory_keys() {
2615            self.u16(*key);
2616        }
2617        self.u64(u64::try_from(service.advertised_alpn().len()).unwrap_or(u64::MAX));
2618        for alpn in service.advertised_alpn() {
2619            self.bytes(alpn);
2620        }
2621        self.u8(service.selected_protocol().tag());
2622        self.u16(service.effective_port().get());
2623        self.u8(service.transport().tag());
2624        self.u64(u64::try_from(service.connection_hints().len()).unwrap_or(u64::MAX));
2625        for hint in service.connection_hints() {
2626            match hint {
2627                IpAddr::V4(address) => {
2628                    self.u8(4);
2629                    self.bytes(&address.octets());
2630                }
2631                IpAddr::V6(address) => {
2632                    self.u8(6);
2633                    self.bytes(&address.octets());
2634                }
2635            }
2636        }
2637        match service.ech_config() {
2638            Some(ech) => {
2639                self.u8(1);
2640                self.bytes(ech);
2641            }
2642            None => self.u8(0),
2643        }
2644        self.u64(u64::try_from(service.parameters().len()).unwrap_or(u64::MAX));
2645        for parameter in service.parameters() {
2646            self.u16(parameter.key());
2647            self.bytes(parameter.value());
2648        }
2649    }
2650}
2651
2652#[allow(
2653    clippy::indexing_slicing,
2654    clippy::many_single_char_names,
2655    clippy::too_many_lines,
2656    reason = "SHA-256 uses the standard eight working variables and fixed algorithm-bounded rounds"
2657)]
2658fn sha256(input: &[u8]) -> [u8; 32] {
2659    const INITIAL: [u32; 8] = [
2660        0x6a09_e667,
2661        0xbb67_ae85,
2662        0x3c6e_f372,
2663        0xa54f_f53a,
2664        0x510e_527f,
2665        0x9b05_688c,
2666        0x1f83_d9ab,
2667        0x5be0_cd19,
2668    ];
2669    const ROUND: [u32; 64] = [
2670        0x428a_2f98,
2671        0x7137_4491,
2672        0xb5c0_fbcf,
2673        0xe9b5_dba5,
2674        0x3956_c25b,
2675        0x59f1_11f1,
2676        0x923f_82a4,
2677        0xab1c_5ed5,
2678        0xd807_aa98,
2679        0x1283_5b01,
2680        0x2431_85be,
2681        0x550c_7dc3,
2682        0x72be_5d74,
2683        0x80de_b1fe,
2684        0x9bdc_06a7,
2685        0xc19b_f174,
2686        0xe49b_69c1,
2687        0xefbe_4786,
2688        0x0fc1_9dc6,
2689        0x240c_a1cc,
2690        0x2de9_2c6f,
2691        0x4a74_84aa,
2692        0x5cb0_a9dc,
2693        0x76f9_88da,
2694        0x983e_5152,
2695        0xa831_c66d,
2696        0xb003_27c8,
2697        0xbf59_7fc7,
2698        0xc6e0_0bf3,
2699        0xd5a7_9147,
2700        0x06ca_6351,
2701        0x1429_2967,
2702        0x27b7_0a85,
2703        0x2e1b_2138,
2704        0x4d2c_6dfc,
2705        0x5338_0d13,
2706        0x650a_7354,
2707        0x766a_0abb,
2708        0x81c2_c92e,
2709        0x9272_2c85,
2710        0xa2bf_e8a1,
2711        0xa81a_664b,
2712        0xc24b_8b70,
2713        0xc76c_51a3,
2714        0xd192_e819,
2715        0xd699_0624,
2716        0xf40e_3585,
2717        0x106a_a070,
2718        0x19a4_c116,
2719        0x1e37_6c08,
2720        0x2748_774c,
2721        0x34b0_bcb5,
2722        0x391c_0cb3,
2723        0x4ed8_aa4a,
2724        0x5b9c_ca4f,
2725        0x682e_6ff3,
2726        0x748f_82ee,
2727        0x78a5_636f,
2728        0x84c8_7814,
2729        0x8cc7_0208,
2730        0x90be_fffa,
2731        0xa450_6ceb,
2732        0xbef9_a3f7,
2733        0xc671_78f2,
2734    ];
2735
2736    let bit_len = u64::try_from(input.len())
2737        .unwrap_or(u64::MAX)
2738        .wrapping_mul(8);
2739    let mut padded = input.to_vec();
2740    padded.push(0x80);
2741    while padded.len() % 64 != 56 {
2742        padded.push(0);
2743    }
2744    padded.extend_from_slice(&bit_len.to_be_bytes());
2745
2746    let mut state = INITIAL;
2747    for chunk in padded.chunks_exact(64) {
2748        let mut words = [0u32; 64];
2749        for (index, bytes) in chunk.chunks_exact(4).enumerate() {
2750            words[index] = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
2751        }
2752        for index in 16..64 {
2753            let s0 = words[index - 15].rotate_right(7)
2754                ^ words[index - 15].rotate_right(18)
2755                ^ (words[index - 15] >> 3);
2756            let s1 = words[index - 2].rotate_right(17)
2757                ^ words[index - 2].rotate_right(19)
2758                ^ (words[index - 2] >> 10);
2759            words[index] = words[index - 16]
2760                .wrapping_add(s0)
2761                .wrapping_add(words[index - 7])
2762                .wrapping_add(s1);
2763        }
2764
2765        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
2766        for index in 0..64 {
2767            let sum1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
2768            let choose = (e & f) ^ ((!e) & g);
2769            let temporary1 = h
2770                .wrapping_add(sum1)
2771                .wrapping_add(choose)
2772                .wrapping_add(ROUND[index])
2773                .wrapping_add(words[index]);
2774            let sum0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
2775            let majority = (a & b) ^ (a & c) ^ (b & c);
2776            let temporary2 = sum0.wrapping_add(majority);
2777            h = g;
2778            g = f;
2779            f = e;
2780            e = d.wrapping_add(temporary1);
2781            d = c;
2782            c = b;
2783            b = a;
2784            a = temporary1.wrapping_add(temporary2);
2785        }
2786        state[0] = state[0].wrapping_add(a);
2787        state[1] = state[1].wrapping_add(b);
2788        state[2] = state[2].wrapping_add(c);
2789        state[3] = state[3].wrapping_add(d);
2790        state[4] = state[4].wrapping_add(e);
2791        state[5] = state[5].wrapping_add(f);
2792        state[6] = state[6].wrapping_add(g);
2793        state[7] = state[7].wrapping_add(h);
2794    }
2795
2796    let mut output = [0u8; 32];
2797    for (index, word) in state.iter().enumerate() {
2798        let start = index * 4;
2799        output[start..start + 4].copy_from_slice(&word.to_be_bytes());
2800    }
2801    output
2802}
2803
2804#[cfg(test)]
2805#[allow(
2806    clippy::expect_used,
2807    clippy::unwrap_used,
2808    reason = "test fixtures fail immediately when their declared invariant is invalid"
2809)]
2810mod tests {
2811    use super::*;
2812    use std::net::{Ipv4Addr, Ipv6Addr};
2813
2814    const NOW: u64 = 1_700_000_000;
2815
2816    fn host(value: &str) -> CanonicalHost {
2817        CanonicalHost::parse(value).unwrap()
2818    }
2819
2820    fn freshness(expires_delta: u64) -> Freshness {
2821        Freshness::new(NOW, NOW + expires_delta).unwrap()
2822    }
2823
2824    fn provenance(namespace: Namespace, variant: u8) -> EvidenceProvenance {
2825        match namespace {
2826            Namespace::Hns => EvidenceProvenance::Hns {
2827                network: HnsNetwork::Mainnet,
2828                tree_root: [variant.wrapping_add(1); 32],
2829                height: u32::from(variant),
2830            },
2831            Namespace::Icann => EvidenceProvenance::IcannDoh {
2832                chain_state: IcannChainState::Secure,
2833            },
2834        }
2835    }
2836
2837    fn service_input(port: u16, protocol: ApplicationProtocol) -> ServiceBindingInput {
2838        let alpn = match protocol {
2839            ApplicationProtocol::Http11 => b"http/1.1".to_vec(),
2840            ApplicationProtocol::Http2 => b"h2".to_vec(),
2841            ApplicationProtocol::Http3 => b"h3".to_vec(),
2842        };
2843        let mut alpn_wire = vec![u8::try_from(alpn.len()).unwrap()];
2844        alpn_wire.extend_from_slice(&alpn);
2845        ServiceBindingInput {
2846            priority: Some(1),
2847            service_target: host("edge.example"),
2848            mandatory_keys: vec![1, 3],
2849            advertised_alpn: vec![alpn],
2850            selected_protocol: protocol,
2851            effective_port: NonZeroU16::new(port).unwrap(),
2852            transport: if protocol == ApplicationProtocol::Http3 {
2853                ServiceTransport::Udp
2854            } else {
2855                ServiceTransport::Tcp
2856            },
2857            connection_hints: Vec::new(),
2858            ech_config: Some(vec![1, 2, 3]),
2859            parameters: vec![
2860                ServiceParameter::new(1, alpn_wire).unwrap(),
2861                ServiceParameter::new(3, port.to_be_bytes().to_vec()).unwrap(),
2862                ServiceParameter::new(5, vec![1, 2, 3]).unwrap(),
2863            ],
2864        }
2865    }
2866
2867    fn service(port: u16, protocol: ApplicationProtocol) -> ServiceBinding {
2868        ServiceBinding::new(service_input(port, protocol)).unwrap()
2869    }
2870
2871    fn default_http11_service_input(
2872        advertised_alpn: &[&[u8]],
2873        no_default_alpn: bool,
2874    ) -> ServiceBindingInput {
2875        let advertised_alpn = advertised_alpn
2876            .iter()
2877            .map(|identifier| identifier.to_vec())
2878            .collect::<Vec<_>>();
2879        let mut alpn_wire = Vec::new();
2880        for identifier in &advertised_alpn {
2881            alpn_wire.push(u8::try_from(identifier.len()).unwrap());
2882            alpn_wire.extend_from_slice(identifier);
2883        }
2884        let mut input = service_input(443, ApplicationProtocol::Http11);
2885        input.advertised_alpn = advertised_alpn;
2886        *input
2887            .parameters
2888            .iter_mut()
2889            .find(|parameter| parameter.key() == 1)
2890            .expect("ALPN parameter") = ServiceParameter::new(1, alpn_wire).unwrap();
2891        if no_default_alpn {
2892            input
2893                .parameters
2894                .push(ServiceParameter::new(2, Vec::new()).unwrap());
2895        }
2896        input
2897    }
2898
2899    fn plan(namespace: Namespace, endpoint_last_octet: u8) -> ValidatedOriginPlan {
2900        let origin = host("www.example");
2901        let terminal = host("edge.example");
2902        ValidatedOriginPlan::new(OriginPlanInput {
2903            namespace,
2904            query: query(),
2905            alias_path: vec![AliasStep::new(AliasKind::Cname, origin, terminal.clone()).unwrap()],
2906            terminal_target: terminal,
2907            endpoint_alias_path: Vec::new(),
2908            endpoint_target: host("edge.example"),
2909            endpoints: vec![
2910                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 443),
2911                SocketAddr::new(
2912                    IpAddr::V4(Ipv4Addr::new(203, 0, 113, endpoint_last_octet)),
2913                    443,
2914                ),
2915            ],
2916            service: service(443, ApplicationProtocol::Http2),
2917            tls_policy: TlsTrustPolicy::Dane,
2918            tlsa_records: vec![
2919                CanonicalTlsa::new({
2920                    let mut rdata = vec![3, 1, 1];
2921                    rdata.extend_from_slice(&[endpoint_last_octet; 32]);
2922                    rdata
2923                })
2924                .unwrap(),
2925            ],
2926            provenance: provenance(namespace, endpoint_last_octet),
2927            freshness: freshness(u64::from(endpoint_last_octet) + 30),
2928        })
2929        .unwrap()
2930    }
2931
2932    fn absence(namespace: Namespace) -> ValidatedAbsence {
2933        let kind = match namespace {
2934            Namespace::Hns => AbsenceKind::HnsCurrentUrkelNonInclusion,
2935            Namespace::Icann => AbsenceKind::DnssecAuthenticatedNxDomain,
2936        };
2937        ValidatedAbsence::new(
2938            namespace,
2939            query(),
2940            kind,
2941            provenance(namespace, 4),
2942            freshness(60),
2943        )
2944        .unwrap()
2945    }
2946
2947    fn query() -> OriginQuery {
2948        OriginQuery::new(
2949            host("www.example"),
2950            OriginScheme::Https,
2951            None,
2952            ProtocolCapabilities::all(),
2953        )
2954    }
2955
2956    #[test]
2957    fn canonical_host_is_syntax_only_and_never_uses_an_iana_list() {
2958        assert_eq!(host("WWW.Example.").as_str(), "www.example");
2959        assert_eq!(host("singlelabel").as_str(), "singlelabel");
2960        for invalid in [
2961            "",
2962            " example",
2963            "example ",
2964            "example..",
2965            "_service.example",
2966            "-bad.example",
2967            "bad-.example",
2968            "127.0.0.1",
2969            "::1",
2970            "bücher.example",
2971        ] {
2972            assert_eq!(
2973                CanonicalHost::parse(invalid),
2974                Err(ValidationError::InvalidHost)
2975            );
2976        }
2977    }
2978
2979    #[test]
2980    fn five_outcome_table_is_explicit() {
2981        let hns = plan(Namespace::Hns, 10);
2982        let icann_same = plan(Namespace::Icann, 10);
2983        let icann_different = plan(Namespace::Icann, 11);
2984
2985        assert!(matches!(
2986            decide_namespace(
2987                &query(),
2988                RootLookup::Present(hns.clone()),
2989                RootLookup::Absent(absence(Namespace::Icann)),
2990                SelectionPolicy::default(),
2991                NOW,
2992            )
2993            .unwrap(),
2994            NamespaceDecision {
2995                outcome: NamespaceOutcome::HnsOnly { .. },
2996                ..
2997            }
2998        ));
2999        assert!(matches!(
3000            decide_namespace(
3001                &query(),
3002                RootLookup::Absent(absence(Namespace::Hns)),
3003                RootLookup::Present(icann_same.clone()),
3004                SelectionPolicy::default(),
3005                NOW,
3006            )
3007            .unwrap(),
3008            NamespaceDecision {
3009                outcome: NamespaceOutcome::IcannOnly { .. },
3010                ..
3011            }
3012        ));
3013        assert!(matches!(
3014            decide_namespace(
3015                &query(),
3016                RootLookup::Present(hns.clone()),
3017                RootLookup::Present(icann_same),
3018                SelectionPolicy::default(),
3019                NOW,
3020            )
3021            .unwrap(),
3022            NamespaceDecision {
3023                outcome: NamespaceOutcome::BothConvergent {
3024                    selected: Namespace::Icann,
3025                    ..
3026                },
3027                ..
3028            }
3029        ));
3030        assert!(matches!(
3031            decide_namespace(
3032                &query(),
3033                RootLookup::Present(hns),
3034                RootLookup::Present(icann_different),
3035                SelectionPolicy::default(),
3036                NOW,
3037            )
3038            .unwrap(),
3039            NamespaceDecision {
3040                outcome: NamespaceOutcome::BothDivergent {
3041                    selected: Namespace::Icann,
3042                    ..
3043                },
3044                ..
3045            }
3046        ));
3047        assert!(matches!(
3048            decide_namespace(
3049                &query(),
3050                RootLookup::Absent(absence(Namespace::Hns)),
3051                RootLookup::Absent(absence(Namespace::Icann)),
3052                SelectionPolicy::default(),
3053                NOW,
3054            )
3055            .unwrap(),
3056            NamespaceDecision {
3057                outcome: NamespaceOutcome::Neither { .. },
3058                ..
3059            }
3060        ));
3061    }
3062
3063    #[test]
3064    fn any_root_failure_prevents_only_or_neither() {
3065        let hns_failure = RootFailure::new(
3066            Namespace::Hns,
3067            query(),
3068            RootFailureKind::StaleHnsAnchor,
3069            None,
3070        );
3071        let error = decide_namespace(
3072            &query(),
3073            RootLookup::Failed(hns_failure.clone()),
3074            RootLookup::Present(plan(Namespace::Icann, 10)),
3075            SelectionPolicy::default(),
3076            NOW,
3077        )
3078        .unwrap_err();
3079        assert_eq!(
3080            error,
3081            ClassificationError::RootFailed {
3082                hns: RootResolutionState::Failed(hns_failure.clone()),
3083                icann: RootResolutionState::Present,
3084            }
3085        );
3086        let hns_state = RootResolutionState::Failed(hns_failure);
3087        assert_eq!(hns_state.disposition(), RootResolutionDisposition::Failed);
3088        assert_eq!(
3089            hns_state.failure().map(RootFailure::kind),
3090            Some(RootFailureKind::StaleHnsAnchor)
3091        );
3092        assert!(hns_state.is_failed());
3093        assert_eq!(
3094            error.to_string(),
3095            "dual-root classification is indeterminate (HNS: failed (StaleHnsAnchor), ICANN: present)"
3096        );
3097
3098        let icann_failure = RootFailure::new(
3099            Namespace::Icann,
3100            query(),
3101            RootFailureKind::BogusDnssec,
3102            None,
3103        );
3104        let error = decide_namespace(
3105            &query(),
3106            RootLookup::Absent(absence(Namespace::Hns)),
3107            RootLookup::Failed(icann_failure.clone()),
3108            SelectionPolicy::default(),
3109            NOW,
3110        )
3111        .unwrap_err();
3112        assert_eq!(
3113            error,
3114            ClassificationError::RootFailed {
3115                hns: RootResolutionState::Absent,
3116                icann: RootResolutionState::Failed(icann_failure),
3117            }
3118        );
3119        assert_eq!(
3120            RootResolutionState::Absent.disposition(),
3121            RootResolutionDisposition::Absent
3122        );
3123        assert_eq!(RootResolutionState::Absent.failure(), None);
3124        assert!(!RootResolutionState::Absent.is_failed());
3125    }
3126
3127    #[test]
3128    fn both_root_failures_retain_their_typed_reasons() {
3129        let hns_failure =
3130            RootFailure::new(Namespace::Hns, query(), RootFailureKind::Transport, None);
3131        let icann_failure = RootFailure::new(
3132            Namespace::Icann,
3133            query(),
3134            RootFailureKind::BogusDnssec,
3135            Some(NOW + 5),
3136        );
3137
3138        assert_eq!(
3139            decide_namespace(
3140                &query(),
3141                RootLookup::Failed(hns_failure.clone()),
3142                RootLookup::Failed(icann_failure.clone()),
3143                SelectionPolicy::default(),
3144                NOW,
3145            ),
3146            Err(ClassificationError::RootFailed {
3147                hns: RootResolutionState::Failed(hns_failure),
3148                icann: RootResolutionState::Failed(icann_failure),
3149            })
3150        );
3151    }
3152
3153    #[test]
3154    fn divergence_precedence_is_pin_then_sticky_then_icann_default() {
3155        let hns = RootLookup::Present(plan(Namespace::Hns, 10));
3156        let icann = RootLookup::Present(plan(Namespace::Icann, 11));
3157
3158        let pinned = decide_namespace(
3159            &query(),
3160            hns.clone(),
3161            icann.clone(),
3162            SelectionPolicy::default().with_explicit_pin(Some(Namespace::Hns)),
3163            NOW,
3164        )
3165        .unwrap();
3166        assert_eq!(pinned.selected_namespace(), Some(Namespace::Hns));
3167        assert_eq!(
3168            pinned.selection_reason(),
3169            Some(SelectionReason::ExplicitPin)
3170        );
3171
3172        let sticky = decide_namespace(
3173            &query(),
3174            hns.clone(),
3175            icann.clone(),
3176            SelectionPolicy::default().with_sticky_binding(Some(Namespace::Hns)),
3177            NOW,
3178        )
3179        .unwrap();
3180        assert_eq!(sticky.selected_namespace(), Some(Namespace::Hns));
3181        assert_eq!(
3182            sticky.selection_reason(),
3183            Some(SelectionReason::StickyBinding)
3184        );
3185
3186        let defaulted =
3187            decide_namespace(&query(), hns, icann, SelectionPolicy::default(), NOW).unwrap();
3188        assert_eq!(defaulted.selected_namespace(), Some(Namespace::Icann));
3189        assert_eq!(
3190            defaulted.selection_reason(),
3191            Some(SelectionReason::IcannDefault)
3192        );
3193    }
3194
3195    #[test]
3196    fn single_root_cannot_silently_replace_a_pin_or_sticky_binding() {
3197        let hns_only = || {
3198            (
3199                RootLookup::Present(plan(Namespace::Hns, 10)),
3200                RootLookup::Absent(absence(Namespace::Icann)),
3201            )
3202        };
3203        let (hns, icann) = hns_only();
3204        assert_eq!(
3205            decide_namespace(
3206                &query(),
3207                hns,
3208                icann,
3209                SelectionPolicy::default().with_explicit_pin(Some(Namespace::Icann)),
3210                NOW,
3211            ),
3212            Err(ClassificationError::SelectedNamespaceUnavailable {
3213                namespace: Namespace::Icann,
3214                selection_reason: SelectionReason::ExplicitPin,
3215            })
3216        );
3217
3218        let (hns, icann) = hns_only();
3219        assert_eq!(
3220            decide_namespace(
3221                &query(),
3222                hns,
3223                icann,
3224                SelectionPolicy::default().with_sticky_binding(Some(Namespace::Icann)),
3225                NOW,
3226            ),
3227            Err(ClassificationError::SelectedNamespaceUnavailable {
3228                namespace: Namespace::Icann,
3229                selection_reason: SelectionReason::StickyBinding,
3230            })
3231        );
3232
3233        let (hns, icann) = hns_only();
3234        let explicitly_hns = decide_namespace(
3235            &query(),
3236            hns,
3237            icann,
3238            SelectionPolicy::default()
3239                .with_sticky_binding(Some(Namespace::Icann))
3240                .with_explicit_pin(Some(Namespace::Hns)),
3241            NOW,
3242        )
3243        .unwrap();
3244        assert_eq!(
3245            explicitly_hns.selection_reason(),
3246            Some(SelectionReason::ExplicitPin)
3247        );
3248    }
3249
3250    #[test]
3251    fn fail_closed_default_requires_selection_only_for_divergence() {
3252        let policy = SelectionPolicy::new(DefaultPrecedence::FailClosed, 1);
3253        assert_eq!(
3254            decide_namespace(
3255                &query(),
3256                RootLookup::Present(plan(Namespace::Hns, 10)),
3257                RootLookup::Present(plan(Namespace::Icann, 11)),
3258                policy,
3259                NOW,
3260            ),
3261            Err(ClassificationError::DivergenceRequiresSelection)
3262        );
3263        assert!(matches!(
3264            decide_namespace(
3265                &query(),
3266                RootLookup::Present(plan(Namespace::Hns, 10)),
3267                RootLookup::Present(plan(Namespace::Icann, 10)),
3268                policy,
3269                NOW,
3270            )
3271            .unwrap(),
3272            NamespaceDecision {
3273                outcome: NamespaceOutcome::BothConvergent { .. },
3274                ..
3275            }
3276        ));
3277    }
3278
3279    #[test]
3280    fn record_order_ttl_and_provenance_do_not_break_convergence() {
3281        let left = plan(Namespace::Hns, 10);
3282        let mut input = OriginPlanInput {
3283            namespace: Namespace::Icann,
3284            query: left.query().clone(),
3285            alias_path: left.alias_path().to_vec(),
3286            terminal_target: left.terminal_target().clone(),
3287            endpoint_alias_path: left.endpoint_alias_path().to_vec(),
3288            endpoint_target: left.endpoint_target().clone(),
3289            endpoints: left.endpoints().iter().rev().copied().collect(),
3290            service: left.service().clone(),
3291            tls_policy: left.tls_policy(),
3292            tlsa_records: left.tlsa_records().iter().rev().cloned().collect(),
3293            provenance: provenance(Namespace::Icann, 99),
3294            freshness: freshness(5),
3295        };
3296        input
3297            .endpoints
3298            .push(*left.endpoints().first().expect("fixture endpoint"));
3299        let right = ValidatedOriginPlan::new(input).unwrap();
3300        assert!(left.equivalent_to(&right));
3301        assert_eq!(left.differences(&right), DivergenceMask::NONE);
3302    }
3303
3304    #[test]
3305    fn partial_endpoint_overlap_and_trust_changes_are_divergent() {
3306        let left = plan(Namespace::Hns, 10);
3307        let right = plan(Namespace::Icann, 11);
3308        assert!(left.differences(&right).contains(DivergenceMask::ENDPOINTS));
3309
3310        let webpki = ValidatedOriginPlan::new(OriginPlanInput {
3311            namespace: Namespace::Icann,
3312            query: left.query().clone(),
3313            alias_path: left.alias_path().to_vec(),
3314            terminal_target: left.terminal_target().clone(),
3315            endpoint_alias_path: left.endpoint_alias_path().to_vec(),
3316            endpoint_target: left.endpoint_target().clone(),
3317            endpoints: left.endpoints().to_vec(),
3318            service: left.service().clone(),
3319            tls_policy: TlsTrustPolicy::WebPkiAuthenticatedAbsence,
3320            tlsa_records: Vec::new(),
3321            provenance: provenance(Namespace::Icann, 1),
3322            freshness: freshness(60),
3323        })
3324        .unwrap();
3325        assert!(
3326            left.differences(&webpki)
3327                .contains(DivergenceMask::TLS_POLICY)
3328        );
3329    }
3330
3331    #[test]
3332    fn service_protocol_and_transport_must_form_one_coherent_plan() {
3333        let mut input = ServiceBindingInput {
3334            priority: Some(1),
3335            service_target: host("edge.example"),
3336            mandatory_keys: Vec::new(),
3337            advertised_alpn: vec![b"h3".to_vec()],
3338            selected_protocol: ApplicationProtocol::Http3,
3339            effective_port: NonZeroU16::new(443).unwrap(),
3340            transport: ServiceTransport::Tcp,
3341            connection_hints: Vec::new(),
3342            ech_config: None,
3343            parameters: vec![ServiceParameter::new(1, vec![2, b'h', b'3']).unwrap()],
3344        };
3345        assert_eq!(
3346            ServiceBinding::new(input.clone()),
3347            Err(ValidationError::InvalidPlan)
3348        );
3349        input.transport = ServiceTransport::Udp;
3350        assert!(ServiceBinding::new(input).is_ok());
3351    }
3352
3353    #[test]
3354    fn http11_default_is_valid_with_nonempty_raw_alpn() {
3355        let input = default_http11_service_input(&[b"h3", b"h2"], false);
3356        let binding = ServiceBinding::new(input).unwrap();
3357
3358        assert_eq!(binding.advertised_alpn(), &[b"h3".to_vec(), b"h2".to_vec()]);
3359        assert_eq!(
3360            service_parameter(binding.parameters(), 1),
3361            Some([2, b'h', b'3', 2, b'h', b'2'].as_slice())
3362        );
3363        assert_eq!(binding.selected_protocol(), ApplicationProtocol::Http11);
3364    }
3365
3366    #[test]
3367    fn no_default_alpn_requires_explicit_http11() {
3368        assert_eq!(
3369            ServiceBinding::new(default_http11_service_input(&[b"h3", b"h2"], true)),
3370            Err(ValidationError::InvalidPlan)
3371        );
3372
3373        let binding = ServiceBinding::new(default_http11_service_input(
3374            &[b"h3", b"h2", b"http/1.1"],
3375            true,
3376        ))
3377        .unwrap();
3378        assert_eq!(binding.selected_protocol(), ApplicationProtocol::Http11);
3379    }
3380
3381    #[test]
3382    fn http2_and_http3_require_explicit_raw_alpn_ids() {
3383        let mut http2 = default_http11_service_input(&[b"h3"], false);
3384        http2.selected_protocol = ApplicationProtocol::Http2;
3385        assert_eq!(
3386            ServiceBinding::new(http2),
3387            Err(ValidationError::InvalidPlan)
3388        );
3389
3390        let mut http3 = default_http11_service_input(&[b"h2"], false);
3391        http3.selected_protocol = ApplicationProtocol::Http3;
3392        http3.transport = ServiceTransport::Udp;
3393        assert_eq!(
3394            ServiceBinding::new(http3),
3395            Err(ValidationError::InvalidPlan)
3396        );
3397    }
3398
3399    #[test]
3400    fn raw_alpn_semantics_remain_in_equivalence_and_fingerprints() {
3401        let plan_with_service = |namespace, service| {
3402            let baseline = plan(namespace, 10);
3403            ValidatedOriginPlan::new(OriginPlanInput {
3404                namespace,
3405                query: baseline.query().clone(),
3406                alias_path: baseline.alias_path().to_vec(),
3407                terminal_target: baseline.terminal_target().clone(),
3408                endpoint_alias_path: baseline.endpoint_alias_path().to_vec(),
3409                endpoint_target: baseline.endpoint_target().clone(),
3410                endpoints: baseline.endpoints().to_vec(),
3411                service,
3412                tls_policy: baseline.tls_policy(),
3413                tlsa_records: baseline.tlsa_records().to_vec(),
3414                provenance: baseline.provenance().clone(),
3415                freshness: baseline.freshness(),
3416            })
3417            .unwrap()
3418        };
3419        let h3_then_h2 = plan_with_service(
3420            Namespace::Hns,
3421            ServiceBinding::new(default_http11_service_input(&[b"h3", b"h2"], false)).unwrap(),
3422        );
3423        let h2_then_h3 = plan_with_service(
3424            Namespace::Hns,
3425            ServiceBinding::new(default_http11_service_input(&[b"h2", b"h3"], false)).unwrap(),
3426        );
3427
3428        assert!(!h3_then_h2.equivalent_to(&h2_then_h3));
3429        let differences = h3_then_h2.differences(&h2_then_h3);
3430        assert!(differences.contains(DivergenceMask::ALPN));
3431        assert!(differences.contains(DivergenceMask::SERVICE_PARAMETERS));
3432
3433        let decision_for = |plan| {
3434            decide_namespace(
3435                &query(),
3436                RootLookup::Present(plan),
3437                RootLookup::Absent(absence(Namespace::Icann)),
3438                SelectionPolicy::default(),
3439                NOW,
3440            )
3441            .unwrap()
3442        };
3443        assert_ne!(
3444            decision_fingerprint(&decision_for(h3_then_h2)),
3445            decision_fingerprint(&decision_for(h2_then_h3))
3446        );
3447    }
3448
3449    #[test]
3450    fn cleartext_origins_reject_service_mode_and_quic_tls_protocols() {
3451        let cleartext_query = OriginQuery::new(
3452            host("www.example"),
3453            OriginScheme::Http,
3454            None,
3455            ProtocolCapabilities::all(),
3456        );
3457        let cleartext_service = ServiceBinding::new(ServiceBindingInput {
3458            priority: None,
3459            service_target: host("edge.example"),
3460            mandatory_keys: Vec::new(),
3461            advertised_alpn: Vec::new(),
3462            selected_protocol: ApplicationProtocol::Http11,
3463            effective_port: NonZeroU16::new(80).unwrap(),
3464            transport: ServiceTransport::Tcp,
3465            connection_hints: Vec::new(),
3466            ech_config: None,
3467            parameters: Vec::new(),
3468        })
3469        .unwrap();
3470        let mut input = OriginPlanInput {
3471            namespace: Namespace::Hns,
3472            query: cleartext_query,
3473            alias_path: vec![
3474                AliasStep::new(AliasKind::Cname, host("www.example"), host("edge.example"))
3475                    .unwrap(),
3476            ],
3477            terminal_target: host("edge.example"),
3478            endpoint_alias_path: Vec::new(),
3479            endpoint_target: host("edge.example"),
3480            endpoints: vec!["203.0.113.10:80".parse().unwrap()],
3481            service: cleartext_service,
3482            tls_policy: TlsTrustPolicy::Cleartext,
3483            tlsa_records: Vec::new(),
3484            provenance: provenance(Namespace::Hns, 10),
3485            freshness: freshness(60),
3486        };
3487        assert!(ValidatedOriginPlan::new(input.clone()).is_ok());
3488
3489        input.service = service(80, ApplicationProtocol::Http3);
3490        assert_eq!(
3491            ValidatedOriginPlan::new(input),
3492            Err(ValidationError::InvalidPlan)
3493        );
3494    }
3495
3496    #[test]
3497    fn service_binding_rejects_alias_mode_and_malformed_mandatory_alpn_or_hints() {
3498        let mut input = service_input(443, ApplicationProtocol::Http2);
3499        input.mandatory_keys = vec![1; MAX_SERVICE_PARAMETERS + 1];
3500        assert_eq!(
3501            ServiceBinding::new(input),
3502            Err(ValidationError::BoundExceeded)
3503        );
3504
3505        let mut input = service_input(443, ApplicationProtocol::Http2);
3506        input.connection_hints = vec![IpAddr::V4(Ipv4Addr::LOCALHOST); MAX_ENDPOINTS + 1];
3507        assert_eq!(
3508            ServiceBinding::new(input),
3509            Err(ValidationError::BoundExceeded)
3510        );
3511
3512        let mut input = service_input(443, ApplicationProtocol::Http2);
3513        input.priority = Some(0);
3514        assert_eq!(
3515            ServiceBinding::new(input),
3516            Err(ValidationError::InvalidPlan)
3517        );
3518
3519        let mut input = service_input(443, ApplicationProtocol::Http2);
3520        input.priority = None;
3521        assert_eq!(
3522            ServiceBinding::new(input),
3523            Err(ValidationError::InvalidPlan)
3524        );
3525
3526        let mut input = service_input(443, ApplicationProtocol::Http2);
3527        input.parameters.retain(|parameter| parameter.key() != 3);
3528        assert_eq!(
3529            ServiceBinding::new(input),
3530            Err(ValidationError::InvalidPlan)
3531        );
3532
3533        let mut input = service_input(443, ApplicationProtocol::Http2);
3534        input.mandatory_keys.push(65_000);
3535        input
3536            .parameters
3537            .push(ServiceParameter::new(65_000, vec![1]).unwrap());
3538        assert_eq!(
3539            ServiceBinding::new(input),
3540            Err(ValidationError::InvalidPlan)
3541        );
3542
3543        let mut input = service_input(443, ApplicationProtocol::Http2);
3544        input.advertised_alpn = vec![b"http/1.1".to_vec()];
3545        *input
3546            .parameters
3547            .iter_mut()
3548            .find(|parameter| parameter.key() == 1)
3549            .expect("ALPN parameter") =
3550            ServiceParameter::new(1, vec![8, b'h', b't', b't', b'p', b'/', b'1', b'.', b'1'])
3551                .unwrap();
3552        assert_eq!(
3553            ServiceBinding::new(input),
3554            Err(ValidationError::InvalidPlan)
3555        );
3556
3557        let mut input = service_input(443, ApplicationProtocol::Http2);
3558        input.connection_hints = vec![IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7))];
3559        input
3560            .parameters
3561            .push(ServiceParameter::new(4, vec![198, 51, 100, 7]).unwrap());
3562        let service = ServiceBinding::new(input).unwrap();
3563        let mut plan_input = OriginPlanInput {
3564            namespace: Namespace::Hns,
3565            query: query(),
3566            alias_path: vec![
3567                AliasStep::new(AliasKind::Cname, host("www.example"), host("edge.example"))
3568                    .unwrap(),
3569            ],
3570            terminal_target: host("edge.example"),
3571            endpoint_alias_path: Vec::new(),
3572            endpoint_target: host("edge.example"),
3573            endpoints: vec!["203.0.113.10:443".parse().unwrap()],
3574            service,
3575            tls_policy: TlsTrustPolicy::Dane,
3576            tlsa_records: plan(Namespace::Hns, 10).tlsa_records().to_vec(),
3577            provenance: provenance(Namespace::Hns, 10),
3578            freshness: freshness(60),
3579        };
3580        assert_eq!(
3581            ValidatedOriginPlan::new(plan_input.clone()),
3582            Err(ValidationError::InvalidPlan)
3583        );
3584        plan_input
3585            .endpoints
3586            .push("198.51.100.7:443".parse().unwrap());
3587        assert!(ValidatedOriginPlan::new(plan_input).is_ok());
3588    }
3589
3590    #[test]
3591    fn service_mode_target_and_complete_origin_query_affect_convergence() {
3592        let left = plan(Namespace::Hns, 10);
3593        let mut target_changed_input = OriginPlanInput {
3594            namespace: Namespace::Icann,
3595            query: left.query().clone(),
3596            alias_path: left.alias_path().to_vec(),
3597            terminal_target: left.terminal_target().clone(),
3598            endpoint_alias_path: left.endpoint_alias_path().to_vec(),
3599            endpoint_target: left.endpoint_target().clone(),
3600            endpoints: left.endpoints().to_vec(),
3601            service: left.service().clone(),
3602            tls_policy: left.tls_policy(),
3603            tlsa_records: left.tlsa_records().to_vec(),
3604            provenance: provenance(Namespace::Icann, 10),
3605            freshness: freshness(60),
3606        };
3607        target_changed_input.service.service_target = host("other-edge.example");
3608        target_changed_input.endpoint_target = host("other-edge.example");
3609        let target_changed = ValidatedOriginPlan::new(target_changed_input).unwrap();
3610        assert!(
3611            left.differences(&target_changed)
3612                .contains(DivergenceMask::SERVICE_TARGET)
3613        );
3614        assert!(
3615            left.differences(&target_changed)
3616                .contains(DivergenceMask::ENDPOINT_TARGET)
3617        );
3618
3619        let endpoint_alias = ValidatedOriginPlan::new(OriginPlanInput {
3620            namespace: Namespace::Icann,
3621            query: left.query().clone(),
3622            alias_path: left.alias_path().to_vec(),
3623            terminal_target: left.terminal_target().clone(),
3624            endpoint_alias_path: vec![
3625                AliasStep::new(
3626                    AliasKind::Cname,
3627                    host("edge.example"),
3628                    host("address.example"),
3629                )
3630                .unwrap(),
3631            ],
3632            endpoint_target: host("address.example"),
3633            endpoints: left.endpoints().to_vec(),
3634            service: left.service().clone(),
3635            tls_policy: left.tls_policy(),
3636            tlsa_records: left.tlsa_records().to_vec(),
3637            provenance: provenance(Namespace::Icann, 10),
3638            freshness: freshness(60),
3639        })
3640        .unwrap();
3641        assert!(
3642            left.differences(&endpoint_alias)
3643                .contains(DivergenceMask::ENDPOINT_ALIAS_PATH)
3644        );
3645
3646        let mut cross_path_cycle = OriginPlanInput {
3647            namespace: Namespace::Icann,
3648            query: left.query().clone(),
3649            alias_path: left.alias_path().to_vec(),
3650            terminal_target: left.terminal_target().clone(),
3651            endpoint_alias_path: Vec::new(),
3652            endpoint_target: host("www.example"),
3653            endpoints: left.endpoints().to_vec(),
3654            service: left.service().clone(),
3655            tls_policy: left.tls_policy(),
3656            tlsa_records: left.tlsa_records().to_vec(),
3657            provenance: provenance(Namespace::Icann, 10),
3658            freshness: freshness(60),
3659        };
3660        cross_path_cycle.service.service_target = host("www.example");
3661        assert_eq!(
3662            ValidatedOriginPlan::new(cross_path_cycle),
3663            Err(ValidationError::InvalidPlan)
3664        );
3665
3666        let mut query_changed = target_changed;
3667        query_changed.query = OriginQuery::new(
3668            host("www.example"),
3669            OriginScheme::Wss,
3670            None,
3671            ProtocolCapabilities::all(),
3672        );
3673        assert!(
3674            left.differences(&query_changed)
3675                .contains(DivergenceMask::ORIGIN_QUERY)
3676        );
3677        assert!(!left.equivalent_to(&query_changed));
3678    }
3679
3680    #[test]
3681    fn tlsa_requires_supported_nonempty_dane_association() {
3682        assert_eq!(
3683            CanonicalTlsa::new(vec![3, 1, 1]),
3684            Err(ValidationError::InvalidPlan)
3685        );
3686        assert_eq!(
3687            CanonicalTlsa::new({
3688                let mut rdata = vec![1, 1, 1];
3689                rdata.extend_from_slice(&[7; 32]);
3690                rdata
3691            }),
3692            Err(ValidationError::InvalidPlan)
3693        );
3694        assert_eq!(
3695            CanonicalTlsa::new({
3696                let mut rdata = vec![3, 1, 1];
3697                rdata.extend_from_slice(&[7; 31]);
3698                rdata
3699            }),
3700            Err(ValidationError::InvalidPlan)
3701        );
3702        assert!(
3703            CanonicalTlsa::new({
3704                let mut rdata = vec![2, 0, 2];
3705                rdata.extend_from_slice(&[7; 64]);
3706                rdata
3707            })
3708            .is_ok()
3709        );
3710    }
3711
3712    #[test]
3713    fn origin_plan_rejects_cross_root_mixing_and_bad_aliases() {
3714        let mut input = OriginPlanInput {
3715            namespace: Namespace::Hns,
3716            query: query(),
3717            alias_path: Vec::new(),
3718            terminal_target: host("other.example"),
3719            endpoint_alias_path: Vec::new(),
3720            endpoint_target: host("edge.example"),
3721            endpoints: vec!["203.0.113.1:443".parse().unwrap()],
3722            service: service(443, ApplicationProtocol::Http2),
3723            tls_policy: TlsTrustPolicy::Dane,
3724            tlsa_records: vec![
3725                CanonicalTlsa::new({
3726                    let mut rdata = vec![3, 1, 1];
3727                    rdata.extend_from_slice(&[1; 32]);
3728                    rdata
3729                })
3730                .unwrap(),
3731            ],
3732            provenance: provenance(Namespace::Icann, 1),
3733            freshness: freshness(60),
3734        };
3735        assert_eq!(
3736            ValidatedOriginPlan::new(input.clone()),
3737            Err(ValidationError::InvalidEvidence)
3738        );
3739        input.provenance = provenance(Namespace::Hns, 1);
3740        assert_eq!(
3741            ValidatedOriginPlan::new(input),
3742            Err(ValidationError::InvalidPlan)
3743        );
3744    }
3745
3746    #[test]
3747    fn evidence_query_and_root_positions_are_bound() {
3748        let wrong_query = OriginQuery::new(
3749            host("www.example"),
3750            OriginScheme::Wss,
3751            None,
3752            ProtocolCapabilities::all(),
3753        );
3754        let wrong_evidence = ValidatedAbsence::new(
3755            Namespace::Icann,
3756            wrong_query,
3757            AbsenceKind::DnssecAuthenticatedNxDomain,
3758            provenance(Namespace::Icann, 1),
3759            freshness(60),
3760        )
3761        .unwrap();
3762        assert_eq!(
3763            decide_namespace(
3764                &query(),
3765                RootLookup::Present(plan(Namespace::Hns, 10)),
3766                RootLookup::Absent(wrong_evidence),
3767                SelectionPolicy::default(),
3768                NOW,
3769            ),
3770            Err(ClassificationError::QueryMismatch {
3771                namespace: Namespace::Icann,
3772            })
3773        );
3774    }
3775
3776    #[test]
3777    fn decision_fingerprint_ignores_freshness_but_binds_selection_and_plan() {
3778        let hns = plan(Namespace::Hns, 10);
3779        let icann = plan(Namespace::Icann, 11);
3780        let icann_selected = decide_namespace(
3781            &query(),
3782            RootLookup::Present(hns.clone()),
3783            RootLookup::Present(icann.clone()),
3784            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
3785            NOW,
3786        )
3787        .unwrap();
3788        let hns_selected = decide_namespace(
3789            &query(),
3790            RootLookup::Present(hns.clone()),
3791            RootLookup::Present(icann.clone()),
3792            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4)
3793                .with_explicit_pin(Some(Namespace::Hns)),
3794            NOW,
3795        )
3796        .unwrap();
3797        let revision_changed = decide_namespace(
3798            &query(),
3799            RootLookup::Present(hns),
3800            RootLookup::Present(icann),
3801            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 5),
3802            NOW,
3803        )
3804        .unwrap();
3805        assert_ne!(
3806            decision_fingerprint(&icann_selected),
3807            decision_fingerprint(&hns_selected)
3808        );
3809        assert_ne!(
3810            decision_fingerprint(&icann_selected),
3811            decision_fingerprint(&revision_changed)
3812        );
3813
3814        let mut refreshed_hns = plan(Namespace::Hns, 10);
3815        refreshed_hns.freshness = freshness(20);
3816        refreshed_hns.provenance = EvidenceProvenance::Hns {
3817            network: HnsNetwork::Mainnet,
3818            tree_root: [77; 32],
3819            height: 77,
3820        };
3821        let mut refreshed_icann = plan(Namespace::Icann, 11);
3822        refreshed_icann.freshness = freshness(25);
3823        let refreshed = decide_namespace(
3824            &query(),
3825            RootLookup::Present(refreshed_hns.clone()),
3826            RootLookup::Present(refreshed_icann),
3827            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
3828            NOW,
3829        )
3830        .unwrap();
3831        assert_eq!(
3832            decision_fingerprint(&icann_selected),
3833            decision_fingerprint(&refreshed)
3834        );
3835
3836        refreshed_hns.provenance = EvidenceProvenance::Hns {
3837            network: HnsNetwork::Testnet,
3838            tree_root: [77; 32],
3839            height: 77,
3840        };
3841        let changed_network = decide_namespace(
3842            &query(),
3843            RootLookup::Present(refreshed_hns),
3844            RootLookup::Present(plan(Namespace::Icann, 11)),
3845            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
3846            NOW,
3847        )
3848        .unwrap();
3849        assert_ne!(
3850            decision_fingerprint(&icann_selected),
3851            decision_fingerprint(&changed_network)
3852        );
3853
3854        let wss_query = OriginQuery::new(
3855            host("www.example"),
3856            OriginScheme::Wss,
3857            None,
3858            ProtocolCapabilities::all(),
3859        );
3860        let mut wss_hns = plan(Namespace::Hns, 10);
3861        let mut wss_icann = plan(Namespace::Icann, 11);
3862        wss_hns.query = wss_query.clone();
3863        wss_icann.query = wss_query.clone();
3864        let wss_decision = decide_namespace(
3865            &wss_query,
3866            RootLookup::Present(wss_hns),
3867            RootLookup::Present(wss_icann),
3868            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
3869            NOW,
3870        )
3871        .unwrap();
3872        assert_ne!(
3873            decision_fingerprint(&icann_selected),
3874            decision_fingerprint(&wss_decision)
3875        );
3876    }
3877
3878    #[test]
3879    fn freshness_uses_earliest_root_expiry() {
3880        let outcome = decide_namespace(
3881            &query(),
3882            RootLookup::Present(plan(Namespace::Hns, 10)),
3883            RootLookup::Present(plan(Namespace::Icann, 11)),
3884            SelectionPolicy::default(),
3885            NOW,
3886        )
3887        .unwrap();
3888        assert_eq!(outcome.expires_at_unix(), NOW + 40);
3889        assert!(outcome.is_fresh_at(NOW + 39));
3890        assert!(!outcome.is_fresh_at(NOW + 40));
3891    }
3892
3893    #[test]
3894    fn convergent_outcome_retains_both_roots_and_their_earliest_expiry() {
3895        let hns = plan(Namespace::Hns, 10);
3896        let mut icann = plan(Namespace::Icann, 10);
3897        icann.freshness = freshness(5);
3898        let outcome = decide_namespace(
3899            &query(),
3900            RootLookup::Present(hns),
3901            RootLookup::Present(icann),
3902            SelectionPolicy::default(),
3903            NOW,
3904        )
3905        .unwrap();
3906        assert_eq!(outcome.expires_at_unix(), NOW + 5);
3907        assert!(matches!(
3908            outcome,
3909            NamespaceDecision {
3910                outcome: NamespaceOutcome::BothConvergent {
3911                    selected: Namespace::Icann,
3912                    hns: ValidatedOriginPlan {
3913                        namespace: Namespace::Hns,
3914                        ..
3915                    },
3916                    icann: ValidatedOriginPlan {
3917                        namespace: Namespace::Icann,
3918                        ..
3919                    },
3920                    ..
3921                },
3922                ..
3923            }
3924        ));
3925    }
3926
3927    #[test]
3928    fn classifier_rejects_stale_evidence_at_the_authority_boundary() {
3929        assert_eq!(
3930            decide_namespace(
3931                &query(),
3932                RootLookup::Present(plan(Namespace::Hns, 10)),
3933                RootLookup::Present(plan(Namespace::Icann, 10)),
3934                SelectionPolicy::default(),
3935                NOW + 40,
3936            ),
3937            Err(ClassificationError::StaleEvidence {
3938                namespace: Namespace::Hns,
3939            })
3940        );
3941    }
3942
3943    #[test]
3944    fn icann_absence_and_tls_policy_require_matching_dnssec_state() {
3945        let insecure = EvidenceProvenance::IcannDoh {
3946            chain_state: IcannChainState::ProvenInsecure,
3947        };
3948        assert_eq!(
3949            ValidatedAbsence::new(
3950                Namespace::Icann,
3951                query(),
3952                AbsenceKind::DnssecAuthenticatedNxDomain,
3953                insecure.clone(),
3954                freshness(60),
3955            ),
3956            Err(ValidationError::InvalidEvidence)
3957        );
3958
3959        let base = plan(Namespace::Icann, 10);
3960        let input = OriginPlanInput {
3961            namespace: Namespace::Icann,
3962            query: base.query().clone(),
3963            alias_path: base.alias_path().to_vec(),
3964            terminal_target: base.terminal_target().clone(),
3965            endpoint_alias_path: base.endpoint_alias_path().to_vec(),
3966            endpoint_target: base.endpoint_target().clone(),
3967            endpoints: base.endpoints().to_vec(),
3968            service: base.service().clone(),
3969            tls_policy: TlsTrustPolicy::Dane,
3970            tlsa_records: base.tlsa_records().to_vec(),
3971            provenance: insecure,
3972            freshness: freshness(60),
3973        };
3974        assert_eq!(
3975            ValidatedOriginPlan::new(input),
3976            Err(ValidationError::InvalidEvidence)
3977        );
3978    }
3979
3980    #[test]
3981    fn cache_key_binds_configuration_and_comparison_schema() {
3982        let decision = decide_namespace(
3983            &query(),
3984            RootLookup::Present(plan(Namespace::Hns, 10)),
3985            RootLookup::Present(plan(Namespace::Icann, 11)),
3986            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
3987            NOW,
3988        )
3989        .unwrap();
3990        let key = DecisionCacheKey::new(&decision, [2; 32], 3);
3991        let changed_configuration = DecisionCacheKey::new(&decision, [3; 32], 3);
3992        let changed_anchor = DecisionCacheKey::new(&decision, [2; 32], 4);
3993
3994        let changed_policy_decision = decide_namespace(
3995            &query(),
3996            RootLookup::Present(plan(Namespace::Hns, 10)),
3997            RootLookup::Present(plan(Namespace::Icann, 11)),
3998            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4)
3999                .with_explicit_pin(Some(Namespace::Hns)),
4000            NOW,
4001        )
4002        .unwrap();
4003        let changed_policy = DecisionCacheKey::new(&changed_policy_decision, [2; 32], 3);
4004
4005        let mut changed_network_hns = plan(Namespace::Hns, 10);
4006        changed_network_hns.provenance = EvidenceProvenance::Hns {
4007            network: HnsNetwork::Testnet,
4008            tree_root: [11; 32],
4009            height: 10,
4010        };
4011        let changed_network_decision = decide_namespace(
4012            &query(),
4013            RootLookup::Present(changed_network_hns),
4014            RootLookup::Present(plan(Namespace::Icann, 11)),
4015            SelectionPolicy::new(DefaultPrecedence::PreferIcann, 4),
4016            NOW,
4017        )
4018        .unwrap();
4019        let changed_network = DecisionCacheKey::new(&changed_network_decision, [2; 32], 3);
4020
4021        assert_eq!(key.query(), decision.query());
4022        assert_eq!(key.hns_network(), HnsNetwork::Mainnet);
4023        assert_eq!(key.decision_fingerprint(), decision_fingerprint(&decision));
4024        assert_ne!(key.fingerprint(), changed_configuration.fingerprint());
4025        assert_ne!(key.fingerprint(), changed_anchor.fingerprint());
4026        assert_ne!(key.fingerprint(), changed_policy.fingerprint());
4027        assert_ne!(key.fingerprint(), changed_network.fingerprint());
4028    }
4029
4030    #[test]
4031    fn internal_sha256_matches_standard_vector() {
4032        assert_eq!(
4033            DecisionFingerprint(sha256(b"abc")).to_hex(),
4034            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
4035        );
4036    }
4037}