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