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