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