1#![forbid(unsafe_code)]
9#![allow(
10 clippy::doc_markdown,
11 clippy::missing_errors_doc,
12 clippy::module_name_repetitions,
13 reason = "protocol acronyms, shared EngineError, and explicit facade names are intentional"
14)]
15
16use std::fmt;
17use std::sync::RwLock;
18
19use hns_browser_observability::{
20 BrowserStatus, DegradedReason, IcannDnssecStatus, IcannTlsAction, Namespace, OutcomeKind,
21 ProviderReadiness, RateLimitState, RevocationReason, RootFailureKind, SelectionReason,
22 StatusError, StatusInput, TransportIdentities, UnsupportedEvidence,
23};
24pub use hns_browser_runtime::{AuthorityState, RuntimeSessionId};
25use hns_browser_runtime::{BrowserRuntime, RuntimeError, RuntimeStamp};
26use hns_dane::{DaneLimits, DaneMatch, verify_dane_chain, verify_dane_ee};
27use hns_dns_wire::{Message, ParseLimits, Query, Rdata, RecordType, Tlsa};
28pub use hns_gateway::{Gateway, GatewayLimits, GatewaySelection};
29pub use hns_p2p_transport::{
30 AdapterFailure, AdmittedDnsResponse, AuthenticatedPeer, DnsRelayRequester,
31 ExperimentalExchange, ExperimentalRequest, ExperimentalResponse, OdohRequester,
32 P2pTransportError, PeerIdentity, RequesterLimits, VerifiedOdohTarget,
33};
34use hns_resolution_policy::{
35 Admission, ChainAnchor, EvidenceState, Network, PolicyConfig, PolicyController, PolicyError,
36 PolicySnapshot, PolicyTransition, ResolutionProvenance, ResolutionTransport, TransportPlan,
37 ValidationEvidence,
38};
39use hns_resolver::ValidatedTlsa;
40
41pub const ENGINE_API_VERSION: u32 = 2;
43pub const MAX_TRANSPORT_IDENTITY_BYTES: usize = 256;
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct EngineConfig {
49 pub runtime_session: RuntimeSessionId,
51 pub network: Network,
53 pub policy: PolicySnapshot,
55}
56
57impl EngineConfig {
58 #[must_use]
60 pub const fn new(
61 runtime_session: RuntimeSessionId,
62 network: Network,
63 policy: PolicySnapshot,
64 ) -> Self {
65 Self {
66 runtime_session,
67 network,
68 policy,
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct EngineSnapshot {
76 pub schema_version: u16,
78 pub runtime_session: [u8; 16],
80 pub runtime_generation: u64,
82 pub event_sequence: u64,
84 pub network: Network,
86 pub authority_state: AuthorityState,
88 pub policy: PolicySnapshot,
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct ObservabilityRuntime {
95 pub registry_fingerprint: [u8; 32],
97 pub protocol_version: u16,
99 pub provider_readiness: ProviderReadiness,
101 pub rate_limits: RateLimitState,
103 pub namespace_outcome: Option<OutcomeKind>,
105 pub hns_root_failure: Option<RootFailureKind>,
107 pub icann_root_failure: Option<RootFailureKind>,
109 pub selected_namespace: Option<Namespace>,
111 pub selection_reason: Option<SelectionReason>,
113 pub decision_fingerprint: Option<[u8; 32]>,
115 pub icann_tls_action: Option<IcannTlsAction>,
119 pub icann_dnssec_status: Option<IcannDnssecStatus>,
121 pub icann_evidence: Option<ValidationEvidence>,
127 pub degraded_reason: Option<DegradedReason>,
129 pub revocation_reason: Option<RevocationReason>,
131 pub unsupported_evidence: Vec<UnsupportedEvidence>,
133}
134
135impl ObservabilityRuntime {
136 #[must_use]
138 pub fn for_policy(policy: PolicySnapshot) -> Self {
139 Self {
140 registry_fingerprint: [0; 32],
141 protocol_version: 0,
142 provider_readiness: ProviderReadiness::from_policy(policy),
143 rate_limits: RateLimitState::default(),
144 namespace_outcome: None,
145 hns_root_failure: None,
146 icann_root_failure: None,
147 selected_namespace: None,
148 selection_reason: None,
149 decision_fingerprint: None,
150 icann_tls_action: None,
151 icann_dnssec_status: None,
152 icann_evidence: None,
153 degraded_reason: None,
154 revocation_reason: None,
155 unsupported_evidence: Vec::new(),
156 }
157 }
158}
159
160impl Default for ObservabilityRuntime {
161 fn default() -> Self {
162 Self::for_policy(PolicySnapshot::default())
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct ResolutionAttempt {
169 runtime_stamp: RuntimeStamp,
170 admission: Admission,
171 query: Query,
172}
173
174impl ResolutionAttempt {
175 #[must_use]
177 pub const fn runtime_session(&self) -> [u8; 16] {
178 self.runtime_stamp.session()
179 }
180
181 #[must_use]
183 pub const fn runtime_generation(&self) -> u64 {
184 self.runtime_stamp.generation()
185 }
186
187 #[must_use]
189 pub const fn policy_generation(&self) -> u64 {
190 self.admission.policy_generation
191 }
192
193 #[must_use]
195 pub const fn transport(&self) -> ResolutionTransport {
196 self.admission.transport
197 }
198
199 #[must_use]
201 pub const fn query(&self) -> &Query {
202 &self.query
203 }
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct ParsedResponse {
209 attempt_stamp: RuntimeStamp,
210 message: Message,
211 untrusted_ad_claim: bool,
212}
213
214impl ParsedResponse {
215 #[must_use]
217 pub const fn message(&self) -> &Message {
218 &self.message
219 }
220
221 #[must_use]
223 pub const fn untrusted_ad_claim(&self) -> bool {
224 self.untrusted_ad_claim
225 }
226}
227
228#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct GatewayResolution {
231 attempt: ResolutionAttempt,
232 response: ParsedResponse,
233 context: CompletionContext,
234}
235
236impl GatewayResolution {
237 #[must_use]
239 pub const fn attempt(&self) -> &ResolutionAttempt {
240 &self.attempt
241 }
242
243 #[must_use]
245 pub const fn response(&self) -> &ParsedResponse {
246 &self.response
247 }
248
249 #[must_use]
251 pub const fn context(&self) -> &CompletionContext {
252 &self.context
253 }
254
255 #[must_use]
257 pub fn into_parts(self) -> (ResolutionAttempt, ParsedResponse, CompletionContext) {
258 (self.attempt, self.response, self.context)
259 }
260}
261
262#[derive(Clone, Debug, Default, Eq, PartialEq)]
264pub struct CompletionContext {
265 pub chain_anchor: Option<ChainAnchor>,
267 pub peer_identity: Option<String>,
269 pub proxy_identity: Option<String>,
271 pub target_identity: Option<String>,
273 pub direct_relay_fallback: bool,
275}
276
277#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282pub struct LocalDanePrerequisites {
283 pub hns_proof: EvidenceState,
285 pub dnssec: EvidenceState,
287 pub chain_current: EvidenceState,
289 pub origin_sni: EvidenceState,
291}
292
293#[derive(Clone, Copy, Debug)]
295pub struct ValidatedDaneInput<'a> {
296 pub validated: &'a ValidatedTlsa,
298 pub certificate_chain_der: &'a [&'a [u8]],
300 pub origin_sni: &'a str,
302 pub validation_unix_time: i64,
304 pub limits: DaneLimits,
306}
307
308impl LocalDanePrerequisites {
309 const fn fully_verified(self) -> bool {
310 matches!(self.hns_proof, EvidenceState::Verified)
311 && matches!(self.dnssec, EvidenceState::Verified)
312 && matches!(self.chain_current, EvidenceState::Verified)
313 && matches!(self.origin_sni, EvidenceState::Verified)
314 }
315}
316
317#[derive(Clone, Eq, PartialEq)]
319pub struct DaneCompletion {
320 provenance: ResolutionProvenance,
321 dane_match: DaneMatch,
322 origin_sni: Option<String>,
323 bridge_valid_from: Option<u64>,
324 bridge_valid_until: Option<u64>,
325}
326
327impl DaneCompletion {
328 #[must_use]
330 pub const fn provenance(&self) -> &ResolutionProvenance {
331 &self.provenance
332 }
333
334 #[must_use]
336 pub const fn dane_match(&self) -> DaneMatch {
337 self.dane_match
338 }
339
340 #[must_use]
342 pub fn origin_sni(&self) -> Option<&str> {
343 self.origin_sni.as_deref()
344 }
345}
346
347impl fmt::Debug for DaneCompletion {
348 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
349 formatter
350 .debug_struct("DaneCompletion")
351 .field("provenance", &self.provenance)
352 .field("dane_match", &self.dane_match)
353 .field(
354 "origin_sni",
355 &self.origin_sni.as_ref().map(|_| "[redacted]"),
356 )
357 .field("bridge_valid_from", &self.bridge_valid_from)
358 .field("bridge_valid_until", &self.bridge_valid_until)
359 .finish()
360 }
361}
362
363#[derive(Clone, Eq, PartialEq)]
365pub struct BrowserBridgeAuthorization {
366 runtime_session: [u8; 16],
367 runtime_generation: u64,
368 policy_generation: u64,
369 event_sequence: u64,
370 valid_from: u64,
371 valid_until: u64,
372 origin: String,
373}
374
375impl BrowserBridgeAuthorization {
376 #[must_use]
378 pub const fn runtime_session(&self) -> [u8; 16] {
379 self.runtime_session
380 }
381
382 #[must_use]
384 pub const fn runtime_generation(&self) -> u64 {
385 self.runtime_generation
386 }
387
388 #[must_use]
390 pub const fn policy_generation(&self) -> u64 {
391 self.policy_generation
392 }
393
394 #[must_use]
396 pub const fn event_sequence(&self) -> u64 {
397 self.event_sequence
398 }
399
400 #[must_use]
402 pub const fn valid_from(&self) -> u64 {
403 self.valid_from
404 }
405
406 #[must_use]
408 pub const fn valid_until(&self) -> u64 {
409 self.valid_until
410 }
411
412 #[must_use]
414 pub fn origin(&self) -> &str {
415 &self.origin
416 }
417}
418
419impl fmt::Debug for BrowserBridgeAuthorization {
420 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
421 formatter
422 .debug_struct("BrowserBridgeAuthorization")
423 .field("runtime_session", &self.runtime_session)
424 .field("runtime_generation", &self.runtime_generation)
425 .field("policy_generation", &self.policy_generation)
426 .field("event_sequence", &self.event_sequence)
427 .field("valid_from", &self.valid_from)
428 .field("valid_until", &self.valid_until)
429 .field("origin", &"[redacted]")
430 .finish()
431 }
432}
433
434#[derive(Debug)]
435struct EngineState {
436 runtime: BrowserRuntime,
437 network: Network,
438 policy: PolicyController,
439 last_provenance: Option<ResolutionProvenance>,
440 last_evidence: ValidationEvidence,
441}
442
443#[derive(Debug)]
445pub struct Engine {
446 state: RwLock<EngineState>,
447}
448
449impl Engine {
450 #[must_use]
452 pub fn new(config: EngineConfig) -> Self {
453 Self {
454 state: RwLock::new(EngineState {
455 runtime: BrowserRuntime::new(config.runtime_session),
456 network: config.network,
457 policy: PolicyController::new(config.policy),
458 last_provenance: None,
459 last_evidence: ValidationEvidence::not_attempted(),
460 }),
461 }
462 }
463
464 pub fn from_persisted(
466 runtime_session: [u8; 16],
467 network: Network,
468 policy: &[u8],
469 ) -> Result<Self, EngineError> {
470 let runtime_session = RuntimeSessionId::new(runtime_session).map_err(map_runtime_error)?;
471 let policy = PolicySnapshot::decode(policy)?;
472 Ok(Self::new(EngineConfig {
473 runtime_session,
474 network,
475 policy,
476 }))
477 }
478
479 pub fn snapshot(&self) -> Result<EngineSnapshot, EngineError> {
481 let state = self.state.read().map_err(|_| EngineError::LockPoisoned)?;
482 let runtime = state.runtime.snapshot();
483 Ok(EngineSnapshot {
484 schema_version: runtime.schema_version(),
485 runtime_session: runtime.session_bytes(),
486 runtime_generation: runtime.generation(),
487 event_sequence: runtime.event_sequence(),
488 network: state.network,
489 authority_state: runtime.authority_state(),
490 policy: state.policy.snapshot(),
491 })
492 }
493
494 pub fn export_policy(&self) -> Result<[u8; 32], EngineError> {
496 Ok(self.snapshot()?.policy.encode())
497 }
498
499 pub fn transport_plan(&self) -> Result<TransportPlan, EngineError> {
501 let state = self.state.read().map_err(|_| EngineError::LockPoisoned)?;
502 Ok(state.policy.transport_plan())
503 }
504
505 pub fn begin_gateway(&self, limits: GatewayLimits) -> Result<Gateway, EngineError> {
507 let state = self.state.read().map_err(|_| EngineError::LockPoisoned)?;
508 Gateway::new(state.policy.snapshot(), limits).map_err(EngineError::Gateway)
509 }
510
511 pub fn observability_status(
513 &self,
514 runtime: ObservabilityRuntime,
515 ) -> Result<BrowserStatus, EngineError> {
516 let state = self.state.read().map_err(|_| EngineError::LockPoisoned)?;
517 let runtime_snapshot = state.runtime.snapshot();
518 let provenance = state.last_provenance.as_ref();
519 let selected_icann = runtime.selected_namespace == Some(Namespace::Icann);
520 let failed_icann = runtime.icann_root_failure.is_some();
521 let icann_context = selected_icann || failed_icann;
522 let classification_failed =
523 runtime.hns_root_failure.is_some() || runtime.icann_root_failure.is_some();
524 let neither = runtime.namespace_outcome == Some(OutcomeKind::Neither);
525 let (chain_anchor, actual_transport, identities, evidence) = if icann_context {
526 let evidence = runtime
527 .icann_evidence
528 .ok_or(EngineError::MissingIcannEvidence)?;
529 (
530 None,
531 ResolutionTransport::ValidatingIcannDoh,
532 TransportIdentities::default(),
533 evidence,
534 )
535 } else if neither || classification_failed {
536 if runtime.icann_evidence.is_some() {
537 return Err(EngineError::UnexpectedIcannEvidence);
538 }
539 (
540 None,
541 ResolutionTransport::Unavailable,
542 TransportIdentities::default(),
543 ValidationEvidence::not_attempted(),
544 )
545 } else {
546 if runtime.icann_evidence.is_some() {
547 return Err(EngineError::UnexpectedIcannEvidence);
548 }
549 let identities = provenance.map_or_else(TransportIdentities::default, |provenance| {
550 TransportIdentities {
551 peer: provenance.peer_identity.clone(),
552 proxy: provenance.proxy_identity.clone(),
553 target: provenance.target_identity.clone(),
554 direct_relay_fallback: provenance.direct_relay_fallback,
555 }
556 });
557 (
558 provenance.and_then(|provenance| provenance.chain_anchor),
559 provenance.map_or(ResolutionTransport::Unavailable, |provenance| {
560 provenance.transport
561 }),
562 identities,
563 provenance.map_or(state.last_evidence, |provenance| provenance.evidence),
564 )
565 };
566 let experimental_p2p = matches!(
567 actual_transport,
568 ResolutionTransport::HandshakeP2pOdoh | ResolutionTransport::HandshakeP2pDnsRelay
569 );
570 BrowserStatus::new(StatusInput {
571 runtime: runtime_snapshot,
572 network: state.network,
573 policy: state.policy.snapshot(),
574 chain_anchor,
575 actual_transport,
576 identities,
577 registry_profile: state.policy.snapshot().config().wire_profile,
578 registry_fingerprint: if experimental_p2p {
579 runtime.registry_fingerprint
580 } else {
581 [0; 32]
582 },
583 protocol_version: if experimental_p2p {
584 runtime.protocol_version
585 } else {
586 0
587 },
588 provider_readiness: runtime.provider_readiness,
589 rate_limits: runtime.rate_limits,
590 evidence,
591 namespace_outcome: runtime.namespace_outcome,
592 hns_root_failure: runtime.hns_root_failure,
593 icann_root_failure: runtime.icann_root_failure,
594 selected_namespace: runtime.selected_namespace,
595 selection_reason: runtime.selection_reason,
596 decision_fingerprint: runtime.decision_fingerprint,
597 icann_tls_action: runtime.icann_tls_action,
598 icann_dnssec_status: runtime.icann_dnssec_status,
599 degraded_reason: runtime.degraded_reason,
600 revocation_reason: runtime.revocation_reason,
601 unsupported_evidence: runtime.unsupported_evidence,
602 })
603 .map_err(EngineError::Status)
604 }
605
606 pub fn update_policy(
608 &self,
609 expected_policy_generation: u64,
610 next: PolicyConfig,
611 ) -> Result<PolicyTransition, EngineError> {
612 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
613 let changed = state.policy.snapshot().config() != next;
614 if changed {
615 state
616 .runtime
617 .ensure_policy_change_capacity()
618 .map_err(map_runtime_error)?;
619 }
620 let transition = state.policy.replace(expected_policy_generation, next)?;
621 if transition.changed {
622 state.runtime.policy_changed().map_err(map_runtime_error)?;
623 state.last_provenance = None;
624 state.last_evidence = ValidationEvidence::revoked();
625 }
626 Ok(transition)
627 }
628
629 pub fn update_policy_blob(
631 &self,
632 expected_policy_generation: u64,
633 blob: &[u8],
634 ) -> Result<PolicyTransition, EngineError> {
635 let decoded = PolicySnapshot::decode(blob)?;
636 if decoded.generation() != expected_policy_generation {
637 return Err(EngineError::Policy(PolicyError::StaleGeneration));
638 }
639 self.update_policy(expected_policy_generation, decoded.config())
640 }
641
642 pub fn advance_authority_state(
644 &self,
645 next: AuthorityState,
646 ) -> Result<EngineSnapshot, EngineError> {
647 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
648 state.runtime.transition(next).map_err(map_runtime_error)?;
649 match next {
650 AuthorityState::Degraded => {
651 state.last_provenance = None;
652 state.last_evidence = unavailable_evidence();
653 }
654 AuthorityState::Revoked | AuthorityState::Stopped => {
655 state.last_provenance = None;
656 state.last_evidence = ValidationEvidence::revoked();
657 }
658 _ => {}
659 }
660 drop(state);
661 self.snapshot()
662 }
663
664 pub fn admit_resolution(
666 &self,
667 transport: ResolutionTransport,
668 query: Query,
669 ) -> Result<ResolutionAttempt, EngineError> {
670 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
671 if !resolution_transport_ready(state.runtime.authority_state()) {
672 return Err(EngineError::AuthorityNotReady);
673 }
674 let admission = state.policy.admit(transport)?;
675 let runtime_stamp = state.runtime.admit_event().map_err(map_runtime_error)?;
676 Ok(ResolutionAttempt {
677 runtime_stamp,
678 admission,
679 query,
680 })
681 }
682
683 pub fn parse_response(
685 &self,
686 attempt: &ResolutionAttempt,
687 response: &[u8],
688 limits: ParseLimits,
689 ) -> Result<ParsedResponse, EngineError> {
690 let state = self.state.read().map_err(|_| EngineError::LockPoisoned)?;
691 ensure_current(&state, attempt)?;
692 let message = Message::parse_with_limits(response, limits)?;
693 let correlated = attempt.query.correlate(&message)?;
694 let untrusted_ad_claim = correlated.untrusted_ad_claim();
695 drop(state);
696 Ok(ParsedResponse {
697 attempt_stamp: attempt.runtime_stamp,
698 message,
699 untrusted_ad_claim,
700 })
701 }
702
703 pub fn admit_gateway_selection(
710 &self,
711 selection: GatewaySelection,
712 query: Query,
713 limits: ParseLimits,
714 ) -> Result<GatewayResolution, EngineError> {
715 let (policy_generation, transport, response, identities, direct_relay_fallback) =
716 selection.into_parts();
717 let message = Message::parse_with_limits(&response, limits)?;
718 let correlated = query.correlate(&message)?;
719 let untrusted_ad_claim = correlated.untrusted_ad_claim();
720 let context = CompletionContext {
721 chain_anchor: None,
722 peer_identity: identities.peer,
723 proxy_identity: identities.proxy,
724 target_identity: identities.target,
725 direct_relay_fallback,
726 };
727 validate_completion_context(transport, &context)?;
728
729 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
730 if state.policy.snapshot().generation() != policy_generation {
731 return Err(EngineError::StaleGatewaySelection);
732 }
733 if !resolution_transport_ready(state.runtime.authority_state()) {
734 return Err(EngineError::AuthorityNotReady);
735 }
736 let admission = state.policy.admit(transport)?;
737 let runtime_stamp = state.runtime.admit_event().map_err(map_runtime_error)?;
738 drop(state);
739
740 Ok(GatewayResolution {
741 response: ParsedResponse {
742 attempt_stamp: runtime_stamp,
743 message,
744 untrusted_ad_claim,
745 },
746 attempt: ResolutionAttempt {
747 runtime_stamp,
748 admission,
749 query,
750 },
751 context,
752 })
753 }
754
755 pub fn complete_resolution_with_local_dane(
761 &self,
762 attempt: &ResolutionAttempt,
763 response: &ParsedResponse,
764 prerequisites: LocalDanePrerequisites,
765 certificate_der: &[u8],
766 limits: DaneLimits,
767 context: CompletionContext,
768 ) -> Result<DaneCompletion, EngineError> {
769 if response.attempt_stamp != attempt.runtime_stamp {
770 return Err(EngineError::ResponseAttemptMismatch);
771 }
772 if response.message.header.flags.rcode() != 0 {
773 return Err(EngineError::UnsuccessfulDnsResponse);
774 }
775 if attempt.query.question.record_type != RecordType::Tlsa
776 || attempt.query.question.class != hns_dns_wire::CLASS_IN
777 {
778 return Err(EngineError::ExpectedTlsaQuery);
779 }
780 if !prerequisites.fully_verified() {
781 return Err(EngineError::Policy(PolicyError::UnverifiedEvidence));
782 }
783 let records = exact_tlsa_answers(attempt, response);
784 let dane_match = verify_dane_ee(certificate_der, &records, limits)?;
785 let evidence = ValidationEvidence {
786 hns_proof: prerequisites.hns_proof,
787 dnssec: prerequisites.dnssec,
788 tlsa: EvidenceState::Verified,
789 dane: EvidenceState::Verified,
790 chain_current: prerequisites.chain_current,
791 origin_sni: prerequisites.origin_sni,
792 };
793 let provenance = self.complete_resolution(attempt, response, evidence, context)?;
794 Ok(DaneCompletion {
795 provenance,
796 dane_match,
797 origin_sni: None,
798 bridge_valid_from: None,
799 bridge_valid_until: None,
800 })
801 }
802
803 pub fn complete_resolution_with_validated_tlsa(
810 &self,
811 attempt: &ResolutionAttempt,
812 response: &ParsedResponse,
813 input: ValidatedDaneInput<'_>,
814 mut context: CompletionContext,
815 ) -> Result<DaneCompletion, EngineError> {
816 if response.attempt_stamp != attempt.runtime_stamp {
817 return Err(EngineError::ResponseAttemptMismatch);
818 }
819 if response.message.header.flags.rcode() != 0 {
820 return Err(EngineError::UnsuccessfulDnsResponse);
821 }
822 if attempt.query.question.record_type != RecordType::Tlsa
823 || attempt.query.question.class != hns_dns_wire::CLASS_IN
824 || attempt.query.question.name != *input.validated.terminal_owner()
825 {
826 return Err(EngineError::ExpectedTlsaQuery);
827 }
828 let hns_authority = input
829 .validated
830 .hns_authority()
831 .ok_or(EngineError::MissingHnsAuthority)?;
832 if input.validation_unix_time != i64::from(hns_authority.validation_time()) {
833 return Err(EngineError::ValidationTimeMismatch);
834 }
835 if network_id(self.snapshot()?.network) != hns_authority.anchor().network().id() {
836 return Err(EngineError::HnsNetworkMismatch);
837 }
838 if input.origin_sni != input.validated.base_domain_ascii() {
839 return Err(EngineError::OriginSniMismatch);
840 }
841 let response_records = exact_tlsa_answers(attempt, response);
842 if response_records != input.validated.records() {
843 return Err(EngineError::ResponseEvidenceMismatch);
844 }
845 let dane_match = verify_dane_chain(
846 input.certificate_chain_der,
847 input.validated.base_domain_ascii(),
848 input.validated.records(),
849 input.validation_unix_time,
850 input.limits,
851 )?;
852 let derived_anchor = ChainAnchor {
853 height: hns_authority.anchor().height().get(),
854 tree_root: hns_authority.anchor().tree_root().into_bytes(),
855 };
856 if context
857 .chain_anchor
858 .is_some_and(|provided| provided != derived_anchor)
859 {
860 return Err(EngineError::ChainAnchorMismatch);
861 }
862 context.chain_anchor = Some(derived_anchor);
863 let evidence = ValidationEvidence {
864 hns_proof: EvidenceState::Verified,
865 dnssec: EvidenceState::Verified,
866 tlsa: EvidenceState::Verified,
867 dane: EvidenceState::Verified,
868 chain_current: EvidenceState::Verified,
869 origin_sni: EvidenceState::Verified,
870 };
871 let provenance = self.complete_resolution(attempt, response, evidence, context)?;
872 Ok(DaneCompletion {
873 provenance,
874 dane_match,
875 origin_sni: Some(input.origin_sni.trim_end_matches('.').to_ascii_lowercase()),
876 bridge_valid_from: Some(hns_authority.anchor().validated_at().get()),
877 bridge_valid_until: Some(hns_authority.anchor().valid_until().get()),
878 })
879 }
880
881 pub fn authorize_browser_bridge(
887 &self,
888 completion: &DaneCompletion,
889 now: u64,
890 ) -> Result<BrowserBridgeAuthorization, EngineError> {
891 let origin = completion
892 .origin_sni
893 .as_ref()
894 .ok_or(EngineError::LegacyCompletionNotBridgeable)?;
895 let valid_from = completion
896 .bridge_valid_from
897 .ok_or(EngineError::LegacyCompletionNotBridgeable)?;
898 let valid_until = completion
899 .bridge_valid_until
900 .ok_or(EngineError::LegacyCompletionNotBridgeable)?;
901 if now < valid_from {
902 return Err(EngineError::CompletionNotYetValid);
903 }
904 if now > valid_until {
905 return Err(EngineError::CompletionExpired);
906 }
907 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
908 let runtime_before = state.runtime.snapshot();
909 if !matches!(
910 runtime_before.authority_state(),
911 AuthorityState::DaneOriginVerified
912 | AuthorityState::BrowserBridgeReady
913 | AuthorityState::Active
914 ) {
915 return Err(EngineError::AuthorityNotReady);
916 }
917 if state.last_provenance.as_ref() != Some(&completion.provenance)
918 || completion.provenance.runtime_session != runtime_before.session_bytes()
919 || completion.provenance.runtime_generation != runtime_before.generation()
920 || completion.provenance.policy_generation != state.policy.snapshot().generation()
921 || !completion.provenance.evidence.fully_verified()
922 {
923 return Err(EngineError::CompletionNotCurrent);
924 }
925 if runtime_before.authority_state() == AuthorityState::DaneOriginVerified {
926 state
927 .runtime
928 .transition(AuthorityState::BrowserBridgeReady)
929 .map_err(map_runtime_error)?;
930 } else {
931 state.runtime.admit_event().map_err(map_runtime_error)?;
932 }
933 let runtime = state.runtime.snapshot();
934 Ok(BrowserBridgeAuthorization {
935 runtime_session: runtime.session_bytes(),
936 runtime_generation: runtime.generation(),
937 policy_generation: state.policy.snapshot().generation(),
938 event_sequence: runtime.event_sequence(),
939 valid_from,
940 valid_until,
941 origin: origin.clone(),
942 })
943 }
944
945 fn complete_resolution(
946 &self,
947 attempt: &ResolutionAttempt,
948 response: &ParsedResponse,
949 evidence: ValidationEvidence,
950 context: CompletionContext,
951 ) -> Result<ResolutionProvenance, EngineError> {
952 validate_completion_context(attempt.transport(), &context)?;
953 let mut state = self.state.write().map_err(|_| EngineError::LockPoisoned)?;
954 ensure_current(&state, attempt)?;
955 if !matches!(
956 state.runtime.authority_state(),
957 AuthorityState::DnssecVerified
958 | AuthorityState::DaneOriginVerified
959 | AuthorityState::BrowserBridgeReady
960 | AuthorityState::Active
961 ) {
962 return Err(EngineError::AuthorityNotReady);
963 }
964 if state.runtime.authority_state() == AuthorityState::DnssecVerified {
965 state
966 .runtime
967 .transition(AuthorityState::DaneOriginVerified)
968 .map_err(map_runtime_error)?;
969 } else {
970 state.runtime.admit_event().map_err(map_runtime_error)?;
971 }
972 let runtime = state.runtime.snapshot();
973 let provenance = ResolutionProvenance {
974 schema_version: 1,
975 runtime_session: runtime.session_bytes(),
976 runtime_generation: runtime.generation(),
977 policy_generation: attempt.admission.policy_generation,
978 event_sequence: runtime.event_sequence(),
979 network: state.network,
980 chain_anchor: context.chain_anchor,
981 transport: attempt.admission.transport,
982 peer_identity: context.peer_identity,
983 proxy_identity: context.proxy_identity,
984 target_identity: context.target_identity,
985 direct_relay_fallback: context.direct_relay_fallback,
986 registry_profile: state.policy.snapshot().config().wire_profile,
987 evidence,
988 untrusted_ad_claim: response.untrusted_ad_claim,
989 };
990 provenance.require_verified_hns_https()?;
991 state.last_provenance = Some(provenance.clone());
992 state.last_evidence = provenance.evidence;
993 Ok(provenance)
994 }
995}
996
997fn exact_tlsa_answers(attempt: &ResolutionAttempt, response: &ParsedResponse) -> Vec<Tlsa> {
998 response
999 .message
1000 .answers
1001 .iter()
1002 .filter(|record| {
1003 record.name == attempt.query.question.name
1004 && record.record_type == RecordType::Tlsa
1005 && record.class == attempt.query.question.class
1006 })
1007 .filter_map(|record| match &record.rdata {
1008 Rdata::Tlsa(tlsa) => Some(tlsa.clone()),
1009 _ => None,
1010 })
1011 .collect()
1012}
1013
1014const fn network_id(network: Network) -> u8 {
1015 match network {
1016 Network::Mainnet => 0,
1017 Network::Testnet => 1,
1018 Network::Regtest => 2,
1019 Network::Simnet => 3,
1020 }
1021}
1022
1023const fn unavailable_evidence() -> ValidationEvidence {
1024 ValidationEvidence {
1025 hns_proof: EvidenceState::Unavailable,
1026 dnssec: EvidenceState::Unavailable,
1027 tlsa: EvidenceState::Unavailable,
1028 dane: EvidenceState::Unavailable,
1029 chain_current: EvidenceState::Unavailable,
1030 origin_sni: EvidenceState::Unavailable,
1031 }
1032}
1033
1034fn ensure_current(state: &EngineState, attempt: &ResolutionAttempt) -> Result<(), EngineError> {
1035 if !state.runtime.admits(attempt.runtime_stamp) {
1036 return Err(EngineError::StaleRuntimeGeneration);
1037 }
1038 state.policy.accept_completion(attempt.admission)?;
1039 Ok(())
1040}
1041
1042const fn resolution_transport_ready(state: AuthorityState) -> bool {
1043 matches!(
1044 state,
1045 AuthorityState::ResolutionTransportReady
1046 | AuthorityState::DnssecVerified
1047 | AuthorityState::DaneOriginVerified
1048 | AuthorityState::BrowserBridgeReady
1049 | AuthorityState::Active
1050 )
1051}
1052
1053const fn map_runtime_error(error: RuntimeError) -> EngineError {
1054 match error {
1055 RuntimeError::ZeroSession => EngineError::InvalidRuntimeSession,
1056 RuntimeError::InvalidAuthorityTransition => EngineError::InvalidAuthorityTransition,
1057 RuntimeError::CounterExhausted => EngineError::GenerationExhausted,
1058 RuntimeError::Stopped | RuntimeError::AuthorityNotReady => EngineError::AuthorityNotReady,
1059 }
1060}
1061
1062fn validate_completion_context(
1063 transport: ResolutionTransport,
1064 context: &CompletionContext,
1065) -> Result<(), EngineError> {
1066 for identity in [
1067 context.peer_identity.as_deref(),
1068 context.proxy_identity.as_deref(),
1069 context.target_identity.as_deref(),
1070 ]
1071 .into_iter()
1072 .flatten()
1073 {
1074 if identity.is_empty() || identity.len() > MAX_TRANSPORT_IDENTITY_BYTES {
1075 return Err(EngineError::InvalidTransportIdentity);
1076 }
1077 }
1078
1079 match transport {
1080 ResolutionTransport::HandshakeP2pOdoh => {
1081 let proxy = context
1082 .proxy_identity
1083 .as_deref()
1084 .ok_or(EngineError::MissingTransportIdentity)?;
1085 let target = context
1086 .target_identity
1087 .as_deref()
1088 .ok_or(EngineError::MissingTransportIdentity)?;
1089 if proxy == target {
1090 return Err(EngineError::ProxyTargetNotSeparated);
1091 }
1092 if context.peer_identity.is_some() || context.direct_relay_fallback {
1093 return Err(EngineError::InvalidCompletionContext);
1094 }
1095 }
1096 ResolutionTransport::HandshakeP2pDnsRelay => {
1097 if context.peer_identity.is_none() {
1098 return Err(EngineError::MissingTransportIdentity);
1099 }
1100 if context.proxy_identity.is_some() || context.target_identity.is_some() {
1101 return Err(EngineError::InvalidCompletionContext);
1102 }
1103 }
1104 ResolutionTransport::DirectAuthoritativeUdp
1105 | ResolutionTransport::DirectAuthoritativeTcp
1106 | ResolutionTransport::AuthenticatedAuthoritativeDoh
1107 | ResolutionTransport::UserConfiguredRecursiveHnsDoh => {
1108 if context.peer_identity.is_some()
1109 || context.proxy_identity.is_some()
1110 || context.target_identity.is_some()
1111 || context.direct_relay_fallback
1112 {
1113 return Err(EngineError::InvalidCompletionContext);
1114 }
1115 }
1116 ResolutionTransport::Unavailable
1117 | ResolutionTransport::ValidatingIcannDoh
1118 | ResolutionTransport::LocalHnsProof => {
1119 return Err(EngineError::InvalidCompletionContext);
1120 }
1121 }
1122 Ok(())
1123}
1124
1125#[derive(Debug)]
1127#[non_exhaustive]
1128pub enum EngineError {
1129 InvalidRuntimeSession,
1131 LockPoisoned,
1133 GenerationExhausted,
1135 InvalidAuthorityTransition,
1137 AuthorityNotReady,
1139 StaleRuntimeGeneration,
1141 StaleGatewaySelection,
1143 ResponseAttemptMismatch,
1145 UnsuccessfulDnsResponse,
1147 ExpectedTlsaQuery,
1149 MissingTransportIdentity,
1151 InvalidTransportIdentity,
1153 ProxyTargetNotSeparated,
1155 InvalidCompletionContext,
1157 ResponseEvidenceMismatch,
1159 OriginSniMismatch,
1161 MissingHnsAuthority,
1163 ValidationTimeMismatch,
1165 HnsNetworkMismatch,
1167 ChainAnchorMismatch,
1169 LegacyCompletionNotBridgeable,
1171 CompletionNotCurrent,
1173 CompletionExpired,
1175 CompletionNotYetValid,
1177 MissingIcannEvidence,
1179 UnexpectedIcannEvidence,
1181 Wire(hns_dns_wire::Error),
1183 Dane(hns_dane::DaneError),
1185 Policy(PolicyError),
1187 Status(StatusError),
1189 Gateway(hns_gateway::GatewayError),
1191}
1192
1193impl fmt::Display for EngineError {
1194 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1195 match self {
1196 Self::InvalidRuntimeSession => formatter.write_str("runtime session must be nonzero"),
1197 Self::LockPoisoned => formatter.write_str("engine state lock poisoned"),
1198 Self::GenerationExhausted => formatter.write_str("engine generation exhausted"),
1199 Self::InvalidAuthorityTransition => {
1200 formatter.write_str("invalid browser authority state transition")
1201 }
1202 Self::AuthorityNotReady => formatter.write_str("browser authority state is not ready"),
1203 Self::StaleRuntimeGeneration => formatter.write_str("stale runtime generation"),
1204 Self::StaleGatewaySelection => {
1205 formatter.write_str("gateway selection belongs to a stale policy generation")
1206 }
1207 Self::ResponseAttemptMismatch => {
1208 formatter.write_str("DNS response and resolution attempt mismatch")
1209 }
1210 Self::UnsuccessfulDnsResponse => {
1211 formatter.write_str("TLSA response has a nonzero DNS response code")
1212 }
1213 Self::ExpectedTlsaQuery => {
1214 formatter.write_str("local DANE completion requires a class-IN TLSA query")
1215 }
1216 Self::MissingTransportIdentity => {
1217 formatter.write_str("required transport identity is missing")
1218 }
1219 Self::InvalidTransportIdentity => {
1220 formatter.write_str("transport identity is empty or too long")
1221 }
1222 Self::ProxyTargetNotSeparated => {
1223 formatter.write_str("ODoH proxy and target identities are not distinct")
1224 }
1225 Self::InvalidCompletionContext => {
1226 formatter.write_str("completion context conflicts with selected transport")
1227 }
1228 Self::ResponseEvidenceMismatch => {
1229 formatter.write_str("validated TLSA evidence does not match terminal response")
1230 }
1231 Self::OriginSniMismatch => {
1232 formatter.write_str("origin SNI does not match the TLSA base domain")
1233 }
1234 Self::MissingHnsAuthority => {
1235 formatter.write_str("validated TLSA lacks on-chain HNS authority evidence")
1236 }
1237 Self::ValidationTimeMismatch => formatter
1238 .write_str("DANE validation time does not match HNS DNSSEC validation time"),
1239 Self::HnsNetworkMismatch => {
1240 formatter.write_str("HNS authority evidence belongs to another network")
1241 }
1242 Self::ChainAnchorMismatch => {
1243 formatter.write_str("completion context conflicts with derived HNS chain anchor")
1244 }
1245 Self::LegacyCompletionNotBridgeable => {
1246 formatter.write_str("legacy DANE completion cannot authorize a browser bridge")
1247 }
1248 Self::CompletionNotCurrent => {
1249 formatter.write_str("DANE completion is not current for this engine")
1250 }
1251 Self::CompletionExpired => {
1252 formatter.write_str("DANE completion chain-currency window expired")
1253 }
1254 Self::CompletionNotYetValid => {
1255 formatter.write_str("DANE completion chain-currency window has not begun")
1256 }
1257 Self::MissingIcannEvidence => formatter
1258 .write_str("ICANN selection or lookup failure requires validation evidence"),
1259 Self::UnexpectedIcannEvidence => {
1260 formatter.write_str("ICANN evidence requires an ICANN selection or lookup failure")
1261 }
1262 Self::Wire(error) => write!(formatter, "DNS wire error: {error}"),
1263 Self::Dane(error) => write!(formatter, "DANE error: {error}"),
1264 Self::Policy(error) => write!(formatter, "policy error: {error}"),
1265 Self::Status(error) => write!(formatter, "observability status error: {error}"),
1266 Self::Gateway(error) => write!(formatter, "transport gateway error: {error}"),
1267 }
1268 }
1269}
1270
1271impl std::error::Error for EngineError {
1272 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1273 match self {
1274 Self::Wire(error) => Some(error),
1275 Self::Dane(error) => Some(error),
1276 Self::Policy(error) => Some(error),
1277 Self::Status(error) => Some(error),
1278 Self::Gateway(error) => Some(error),
1279 _ => None,
1280 }
1281 }
1282}
1283
1284impl From<hns_dns_wire::Error> for EngineError {
1285 fn from(value: hns_dns_wire::Error) -> Self {
1286 Self::Wire(value)
1287 }
1288}
1289
1290impl From<hns_dane::DaneError> for EngineError {
1291 fn from(value: hns_dane::DaneError) -> Self {
1292 Self::Dane(value)
1293 }
1294}
1295
1296impl From<PolicyError> for EngineError {
1297 fn from(value: PolicyError) -> Self {
1298 Self::Policy(value)
1299 }
1300}
1301
1302#[cfg(test)]
1303#[allow(
1304 clippy::unwrap_used,
1305 reason = "tests intentionally fail immediately on invalid fixtures"
1306)]
1307mod tests {
1308 use super::*;
1309 use hns_browser_testkit::{STRICT_HNS_ORIGIN, StrictRegtestDaneFixture};
1310 use hns_dns_wire::{Flags, Header, Name, ResourceRecord};
1311 use hns_resolution_policy::{DnsRelayRequesterPolicy, EvidenceState, ObliviousDnsPolicy};
1312
1313 const RESPONSE_WITH_UNTRUSTED_AD: &[u8] =
1314 b"\x12\x34\x84\x20\x00\x01\x00\x01\x00\x00\x00\x00\x07example\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04\x7f\x00\x00\x01";
1315
1316 fn ready_engine_in_session(runtime_session: [u8; 16], network: Network) -> Engine {
1317 let engine = Engine::new(EngineConfig {
1318 runtime_session: RuntimeSessionId::new(runtime_session).unwrap(),
1319 network,
1320 policy: PolicySnapshot::default(),
1321 });
1322 for state in [
1323 AuthorityState::LocalStateOpened,
1324 AuthorityState::HeaderSyncing,
1325 AuthorityState::HeaderCurrent,
1326 AuthorityState::ProofReady,
1327 AuthorityState::ResolutionTransportReady,
1328 AuthorityState::DnssecVerified,
1329 ] {
1330 engine.advance_authority_state(state).unwrap();
1331 }
1332 engine
1333 }
1334
1335 fn ready_engine_on(network: Network) -> Engine {
1336 ready_engine_in_session([7; 16], network)
1337 }
1338
1339 fn ready_engine() -> Engine {
1340 ready_engine_on(Network::Mainnet)
1341 }
1342
1343 fn active_engine_without_dane() -> Engine {
1344 let engine = ready_engine_in_session([13; 16], Network::Mainnet);
1345 engine
1346 .advance_authority_state(AuthorityState::BrowserBridgeReady)
1347 .unwrap();
1348 engine
1349 .advance_authority_state(AuthorityState::Active)
1350 .unwrap();
1351 engine
1352 }
1353
1354 fn icann_observability(
1355 action: IcannTlsAction,
1356 evidence: Option<ValidationEvidence>,
1357 ) -> ObservabilityRuntime {
1358 ObservabilityRuntime {
1359 registry_fingerprint: [99; 32],
1360 protocol_version: 99,
1361 namespace_outcome: Some(OutcomeKind::IcannOnly),
1362 selected_namespace: Some(Namespace::Icann),
1363 selection_reason: Some(SelectionReason::SingleRoot),
1364 decision_fingerprint: Some([13; 32]),
1365 icann_tls_action: Some(action),
1366 icann_dnssec_status: Some(if action == IcannTlsAction::WebPkiInsecureDelegation {
1367 IcannDnssecStatus::InsecureDelegation
1368 } else {
1369 IcannDnssecStatus::Secure
1370 }),
1371 icann_evidence: evidence,
1372 ..ObservabilityRuntime::default()
1373 }
1374 }
1375
1376 fn failed_icann_observability(
1377 failure: RootFailureKind,
1378 dnssec_status: Option<IcannDnssecStatus>,
1379 evidence: ValidationEvidence,
1380 ) -> ObservabilityRuntime {
1381 ObservabilityRuntime {
1382 registry_fingerprint: [99; 32],
1383 protocol_version: 99,
1384 icann_root_failure: Some(failure),
1385 icann_tls_action: Some(IcannTlsAction::FailClosed),
1386 icann_dnssec_status: dnssec_status,
1387 icann_evidence: Some(evidence),
1388 ..ObservabilityRuntime::default()
1389 }
1390 }
1391
1392 fn decode_hex(input: &str) -> Vec<u8> {
1393 let compact: Vec<u8> = input
1394 .bytes()
1395 .filter(|byte| !byte.is_ascii_whitespace())
1396 .collect();
1397 assert!(compact.len().is_multiple_of(2));
1398 compact
1399 .chunks_exact(2)
1400 .map(|pair| {
1401 let high = char::from(*pair.first().unwrap()).to_digit(16).unwrap();
1402 let low = char::from(*pair.get(1).unwrap()).to_digit(16).unwrap();
1403 u8::try_from((high << 4) | low).unwrap()
1404 })
1405 .collect()
1406 }
1407
1408 fn certificate() -> Vec<u8> {
1409 decode_hex(include_str!(
1410 "../../../fixtures/dane/self-signed-cert.der.hex"
1411 ))
1412 }
1413
1414 fn verified_prerequisites() -> LocalDanePrerequisites {
1415 LocalDanePrerequisites {
1416 hns_proof: EvidenceState::Verified,
1417 dnssec: EvidenceState::Verified,
1418 chain_current: EvidenceState::Verified,
1419 origin_sni: EvidenceState::Verified,
1420 }
1421 }
1422
1423 fn tlsa_exchange(tlsa: Tlsa) -> (Query, Vec<u8>) {
1424 let query = Query::new(
1425 0x1234,
1426 Name::from_ascii("_443._tcp.example").unwrap(),
1427 RecordType::Tlsa,
1428 )
1429 .unwrap();
1430 let response = Message {
1431 header: Header {
1432 id: query.id,
1433 flags: Flags::from_bits(0x8420),
1434 question_count: 1,
1435 answer_count: 1,
1436 authority_count: 0,
1437 additional_count: 0,
1438 },
1439 questions: vec![query.question.clone()],
1440 answers: vec![ResourceRecord {
1441 name: query.question.name.clone(),
1442 record_type: RecordType::Tlsa,
1443 class: hns_dns_wire::CLASS_IN,
1444 ttl: 300,
1445 rdata: Rdata::Tlsa(tlsa),
1446 }],
1447 authorities: Vec::new(),
1448 additionals: Vec::new(),
1449 }
1450 .encode(u16::MAX.into())
1451 .unwrap();
1452 (query, response)
1453 }
1454
1455 fn exact_certificate_exchange() -> (Query, Vec<u8>, Vec<u8>) {
1456 let certificate = certificate();
1457 let (query, response) = tlsa_exchange(Tlsa {
1458 usage: 3,
1459 selector: 0,
1460 matching_type: 0,
1461 association_data: certificate.clone(),
1462 });
1463 (query, response, certificate)
1464 }
1465
1466 fn odoh_gateway_selection(engine: &Engine, response: &[u8]) -> Option<GatewaySelection> {
1467 let policy = engine.snapshot().unwrap().policy;
1468 let mut gateway = engine.begin_gateway(GatewayLimits::default()).unwrap();
1469 let mut now = 100_u64;
1470 loop {
1471 let attempt = gateway.next_attempt(policy, now).unwrap();
1472 let outcome = if attempt.transport() == ResolutionTransport::HandshakeP2pOdoh {
1473 hns_gateway::AttemptOutcome::Response {
1474 bytes: response.to_owned(),
1475 identities: hns_gateway::GatewayIdentities {
1476 proxy: Some("brontide:proxy".to_owned()),
1477 target: Some("brontide:target".to_owned()),
1478 ..hns_gateway::GatewayIdentities::default()
1479 },
1480 }
1481 } else {
1482 hns_gateway::AttemptOutcome::Failure(hns_gateway::TransportFailure::Unsupported)
1483 };
1484 match gateway.complete(policy, attempt, outcome, now).unwrap() {
1485 hns_gateway::GatewayStep::RetryAvailable => {
1486 now = now.checked_add(1).unwrap();
1487 }
1488 hns_gateway::GatewayStep::Selected(selection) => return Some(selection),
1489 hns_gateway::GatewayStep::Unavailable => return None,
1490 }
1491 }
1492 }
1493
1494 #[test]
1495 fn correlates_then_derives_local_dane_evidence() {
1496 let engine = ready_engine();
1497 let (query, response, certificate) = exact_certificate_exchange();
1498 let attempt = engine
1499 .admit_resolution(ResolutionTransport::HandshakeP2pOdoh, query)
1500 .unwrap();
1501 let parsed = engine
1502 .parse_response(&attempt, &response, ParseLimits::requester())
1503 .unwrap();
1504 let completed = engine
1505 .complete_resolution_with_local_dane(
1506 &attempt,
1507 &parsed,
1508 verified_prerequisites(),
1509 &certificate,
1510 DaneLimits::default(),
1511 CompletionContext {
1512 proxy_identity: Some("proxy-peer".to_owned()),
1513 target_identity: Some("target-peer".to_owned()),
1514 ..CompletionContext::default()
1515 },
1516 )
1517 .unwrap();
1518
1519 assert!(completed.provenance().untrusted_ad_claim);
1520 assert!(completed.provenance().evidence.fully_verified());
1521 assert_eq!(completed.dane_match().record_index(), 0);
1522 assert_eq!(completed.dane_match().selector() as u8, 0);
1523 assert_eq!(completed.dane_match().matching_type() as u8, 0);
1524 assert_eq!(completed.origin_sni(), None);
1525 assert!(matches!(
1526 engine.authorize_browser_bridge(&completed, 0),
1527 Err(EngineError::LegacyCompletionNotBridgeable)
1528 ));
1529 assert_eq!(
1530 engine.snapshot().unwrap().authority_state,
1531 AuthorityState::DaneOriginVerified
1532 );
1533 }
1534
1535 #[test]
1536 fn rejects_attempt_replay_from_another_runtime_session() {
1537 let first = ready_engine_in_session([7; 16], Network::Mainnet);
1538 let second = ready_engine_in_session([8; 16], Network::Mainnet);
1539 let (query, response, _) = exact_certificate_exchange();
1540 let attempt = first
1541 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1542 .unwrap();
1543 assert_eq!(attempt.runtime_session(), [7; 16]);
1544 assert!(
1545 first
1546 .parse_response(&attempt, &response, ParseLimits::requester())
1547 .is_ok()
1548 );
1549 assert!(matches!(
1550 second.parse_response(&attempt, &response, ParseLimits::requester()),
1551 Err(EngineError::StaleRuntimeGeneration)
1552 ));
1553 }
1554
1555 #[test]
1556 #[allow(
1557 clippy::too_many_lines,
1558 reason = "the end-to-end test keeps DNSSEC, resolver, SNI, and DANE evidence in one flow"
1559 )]
1560 fn engine_consumes_local_dnssec_tlsa_and_rejects_sni_mismatch() {
1561 let engine = ready_engine_on(Network::Regtest);
1562 let fixture = StrictRegtestDaneFixture::new().unwrap();
1563 let validation_time = fixture.validation_time();
1564 let attempt = engine
1565 .admit_resolution(
1566 ResolutionTransport::DirectAuthoritativeTcp,
1567 fixture.query().clone(),
1568 )
1569 .unwrap();
1570 let parsed = engine
1571 .parse_response(&attempt, fixture.response(), ParseLimits::requester())
1572 .unwrap();
1573 let validated = fixture.validate_response(parsed.message()).unwrap();
1574 let chain = [fixture.certificate()];
1575 assert!(matches!(
1576 engine.complete_resolution_with_validated_tlsa(
1577 &attempt,
1578 &parsed,
1579 ValidatedDaneInput {
1580 validated: &validated,
1581 certificate_chain_der: &chain,
1582 origin_sni: "wrong.alpha",
1583 validation_unix_time: i64::from(validation_time),
1584 limits: DaneLimits::default(),
1585 },
1586 CompletionContext::default(),
1587 ),
1588 Err(EngineError::OriginSniMismatch)
1589 ));
1590 let completed = engine
1591 .complete_resolution_with_validated_tlsa(
1592 &attempt,
1593 &parsed,
1594 ValidatedDaneInput {
1595 validated: &validated,
1596 certificate_chain_der: &chain,
1597 origin_sni: STRICT_HNS_ORIGIN,
1598 validation_unix_time: i64::from(validation_time),
1599 limits: DaneLimits::default(),
1600 },
1601 CompletionContext::default(),
1602 )
1603 .unwrap();
1604 assert!(completed.provenance().evidence.fully_verified());
1605 assert!(completed.provenance().untrusted_ad_claim);
1606 assert_eq!(
1607 completed.provenance().chain_anchor,
1608 Some(ChainAnchor {
1609 height: 1,
1610 tree_root: fixture.authority().anchor().tree_root().into_bytes(),
1611 })
1612 );
1613 assert_eq!(
1614 completed.dane_match().usage(),
1615 hns_dane::CertificateUsage::DaneEe
1616 );
1617 assert_eq!(completed.origin_sni(), Some(STRICT_HNS_ORIGIN));
1618 assert!(matches!(
1619 engine.authorize_browser_bridge(&completed, u64::from(validation_time - 1)),
1620 Err(EngineError::CompletionNotYetValid)
1621 ));
1622 let bridge = engine
1623 .authorize_browser_bridge(&completed, u64::from(validation_time))
1624 .unwrap();
1625 assert_eq!(bridge.origin(), STRICT_HNS_ORIGIN);
1626 assert_eq!(bridge.runtime_session(), [7; 16]);
1627 assert_eq!(bridge.valid_from(), u64::from(validation_time));
1628 assert!(bridge.valid_until() >= u64::from(validation_time));
1629 assert!(matches!(
1630 engine.authorize_browser_bridge(&completed, bridge.valid_until() + 1),
1631 Err(EngineError::CompletionExpired)
1632 ));
1633 assert_eq!(
1634 engine.snapshot().unwrap().authority_state,
1635 AuthorityState::BrowserBridgeReady
1636 );
1637 assert!(!format!("{bridge:?}").contains(STRICT_HNS_ORIGIN));
1638 let status = engine
1639 .observability_status(ObservabilityRuntime {
1640 registry_fingerprint: [8; 32],
1641 protocol_version: 1,
1642 ..ObservabilityRuntime::default()
1643 })
1644 .unwrap();
1645 assert_eq!(
1646 status.actual_transport(),
1647 ResolutionTransport::DirectAuthoritativeTcp
1648 );
1649 assert_eq!(status.chain_anchor(), completed.provenance().chain_anchor);
1650 assert!(status.evidence().fully_verified());
1651 assert_eq!(status.registry_fingerprint(), [0; 32]);
1652 assert_eq!(status.protocol_version(), 0);
1653 }
1654
1655 #[test]
1656 fn observability_requires_reasons_and_clears_verified_state_when_degraded() {
1657 let engine = ready_engine();
1658 let initial = engine
1659 .observability_status(ObservabilityRuntime::default())
1660 .unwrap();
1661 assert_eq!(initial.actual_transport(), ResolutionTransport::Unavailable);
1662 assert_eq!(initial.evidence(), ValidationEvidence::not_attempted());
1663
1664 engine
1665 .advance_authority_state(AuthorityState::Degraded)
1666 .unwrap();
1667 assert!(matches!(
1668 engine.observability_status(ObservabilityRuntime::default()),
1669 Err(EngineError::Status(StatusError::MissingFailureReason))
1670 ));
1671 let degraded = engine
1672 .observability_status(ObservabilityRuntime {
1673 degraded_reason: Some(DegradedReason::HeaderSyncUnavailable),
1674 ..ObservabilityRuntime::default()
1675 })
1676 .unwrap();
1677 assert_eq!(
1678 degraded.degraded_reason(),
1679 Some(DegradedReason::HeaderSyncUnavailable)
1680 );
1681 assert_eq!(degraded.evidence().hns_proof, EvidenceState::Unavailable);
1682 }
1683
1684 #[test]
1685 fn observability_facade_reports_selected_icann_dane_and_webpki() {
1686 let engine = active_engine_without_dane();
1687
1688 let mut dane = ValidationEvidence::not_attempted();
1689 dane.dnssec = EvidenceState::Verified;
1690 dane.tlsa = EvidenceState::Verified;
1691 dane.dane = EvidenceState::Verified;
1692 let status = engine
1693 .observability_status(icann_observability(IcannTlsAction::EnforceDane, Some(dane)))
1694 .unwrap();
1695 assert_eq!(
1696 status.actual_transport(),
1697 ResolutionTransport::ValidatingIcannDoh
1698 );
1699 assert_eq!(status.chain_anchor(), None);
1700 assert_eq!(status.identities(), &TransportIdentities::default());
1701 assert_eq!(status.registry_fingerprint(), [0; 32]);
1702 assert_eq!(status.protocol_version(), 0);
1703 assert_eq!(status.evidence(), dane);
1704
1705 let mut authenticated_absence = ValidationEvidence::not_attempted();
1706 authenticated_absence.dnssec = EvidenceState::Verified;
1707 authenticated_absence.tlsa = EvidenceState::Unavailable;
1708 let status = engine
1709 .observability_status(icann_observability(
1710 IcannTlsAction::WebPkiAuthenticatedAbsence,
1711 Some(authenticated_absence),
1712 ))
1713 .unwrap();
1714 assert_eq!(
1715 status.icann_tls_action(),
1716 Some(IcannTlsAction::WebPkiAuthenticatedAbsence)
1717 );
1718 assert_eq!(status.evidence().dane, EvidenceState::NotAttempted);
1719
1720 let mut proven_insecure = authenticated_absence;
1721 proven_insecure.dane = EvidenceState::Unavailable;
1722 let status = engine
1723 .observability_status(icann_observability(
1724 IcannTlsAction::WebPkiInsecureDelegation,
1725 Some(proven_insecure),
1726 ))
1727 .unwrap();
1728 assert_eq!(
1729 status.icann_tls_action(),
1730 Some(IcannTlsAction::WebPkiInsecureDelegation)
1731 );
1732 }
1733
1734 #[test]
1735 fn observability_facade_keeps_bogus_and_indeterminate_icann_fail_closed() {
1736 let engine = active_engine_without_dane();
1737
1738 let mut bogus = ValidationEvidence::not_attempted();
1739 bogus.dnssec = EvidenceState::Failed;
1740 let status = engine
1741 .observability_status(failed_icann_observability(
1742 RootFailureKind::BogusDnssec,
1743 Some(IcannDnssecStatus::Bogus),
1744 bogus,
1745 ))
1746 .unwrap();
1747 assert_eq!(status.namespace_outcome(), None);
1748 assert_eq!(status.selected_namespace(), None);
1749 assert_eq!(
1750 status.icann_root_failure(),
1751 Some(RootFailureKind::BogusDnssec)
1752 );
1753 assert_eq!(status.icann_tls_action(), Some(IcannTlsAction::FailClosed));
1754 assert_eq!(status.evidence().dnssec, EvidenceState::Failed);
1755
1756 let mut missing_action = failed_icann_observability(
1757 RootFailureKind::BogusDnssec,
1758 Some(IcannDnssecStatus::Bogus),
1759 bogus,
1760 );
1761 missing_action.icann_tls_action = None;
1762 assert!(matches!(
1763 engine.observability_status(missing_action),
1764 Err(EngineError::Status(StatusError::InvalidIcannTlsContext))
1765 ));
1766
1767 let mut indeterminate = ValidationEvidence::not_attempted();
1768 indeterminate.dnssec = EvidenceState::Unavailable;
1769 indeterminate.tlsa = EvidenceState::Unavailable;
1770 indeterminate.dane = EvidenceState::Unavailable;
1771 let status = engine
1772 .observability_status(failed_icann_observability(
1773 RootFailureKind::IndeterminateDnssec,
1774 Some(IcannDnssecStatus::Indeterminate),
1775 indeterminate,
1776 ))
1777 .unwrap();
1778 assert_eq!(status.evidence().dnssec, EvidenceState::Unavailable);
1779 assert_eq!(status.icann_tls_action(), Some(IcannTlsAction::FailClosed));
1780 }
1781
1782 #[test]
1783 fn observability_facade_requires_evidence_for_selected_or_failed_icann() {
1784 let engine = active_engine_without_dane();
1785 assert!(matches!(
1786 engine.observability_status(icann_observability(
1787 IcannTlsAction::WebPkiAuthenticatedAbsence,
1788 None,
1789 )),
1790 Err(EngineError::MissingIcannEvidence)
1791 ));
1792
1793 let runtime = ObservabilityRuntime {
1794 icann_evidence: Some(ValidationEvidence::not_attempted()),
1795 ..ObservabilityRuntime::default()
1796 };
1797 assert!(matches!(
1798 engine.observability_status(runtime),
1799 Err(EngineError::UnexpectedIcannEvidence)
1800 ));
1801
1802 let mut failed = failed_icann_observability(
1803 RootFailureKind::BogusDnssec,
1804 Some(IcannDnssecStatus::Bogus),
1805 ValidationEvidence {
1806 dnssec: EvidenceState::Failed,
1807 ..ValidationEvidence::not_attempted()
1808 },
1809 );
1810 failed.icann_evidence = None;
1811 assert!(matches!(
1812 engine.observability_status(failed),
1813 Err(EngineError::MissingIcannEvidence)
1814 ));
1815 }
1816
1817 #[test]
1818 fn neither_outcome_cannot_reuse_prior_hns_provenance() {
1819 let engine = ready_engine();
1820 let (query, response, certificate) = exact_certificate_exchange();
1821 let attempt = engine
1822 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1823 .unwrap();
1824 let parsed = engine
1825 .parse_response(&attempt, &response, ParseLimits::requester())
1826 .unwrap();
1827 engine
1828 .complete_resolution_with_local_dane(
1829 &attempt,
1830 &parsed,
1831 verified_prerequisites(),
1832 &certificate,
1833 DaneLimits::default(),
1834 CompletionContext::default(),
1835 )
1836 .unwrap();
1837
1838 let status = engine
1839 .observability_status(ObservabilityRuntime {
1840 namespace_outcome: Some(OutcomeKind::Neither),
1841 decision_fingerprint: Some([21; 32]),
1842 ..ObservabilityRuntime::default()
1843 })
1844 .unwrap();
1845 assert_eq!(status.actual_transport(), ResolutionTransport::Unavailable);
1846 assert_eq!(status.chain_anchor(), None);
1847 assert_eq!(status.identities(), &TransportIdentities::default());
1848 assert_eq!(status.evidence(), ValidationEvidence::not_attempted());
1849 }
1850
1851 #[test]
1852 fn rejects_non_tlsa_queries_wrong_owner_and_certificate_mismatch() {
1853 let engine = ready_engine();
1854 let query =
1855 Query::new(0x1234, Name::from_ascii("example").unwrap(), RecordType::A).unwrap();
1856 let attempt = engine
1857 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1858 .unwrap();
1859 let parsed = engine
1860 .parse_response(
1861 &attempt,
1862 RESPONSE_WITH_UNTRUSTED_AD,
1863 ParseLimits::requester(),
1864 )
1865 .unwrap();
1866 assert!(matches!(
1867 engine.complete_resolution_with_local_dane(
1868 &attempt,
1869 &parsed,
1870 verified_prerequisites(),
1871 &certificate(),
1872 DaneLimits::default(),
1873 CompletionContext::default(),
1874 ),
1875 Err(EngineError::ExpectedTlsaQuery)
1876 ));
1877
1878 let certificate = certificate();
1879 let (query, mut response) = tlsa_exchange(Tlsa {
1880 usage: 3,
1881 selector: 0,
1882 matching_type: 0,
1883 association_data: certificate.clone(),
1884 });
1885 *response.get_mut(3).unwrap() |= 3;
1886 let attempt = engine
1887 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1888 .unwrap();
1889 let parsed = engine
1890 .parse_response(&attempt, &response, ParseLimits::requester())
1891 .unwrap();
1892 assert!(matches!(
1893 engine.complete_resolution_with_local_dane(
1894 &attempt,
1895 &parsed,
1896 verified_prerequisites(),
1897 &certificate,
1898 DaneLimits::default(),
1899 CompletionContext::default(),
1900 ),
1901 Err(EngineError::UnsuccessfulDnsResponse)
1902 ));
1903
1904 let (query, mut response) = tlsa_exchange(Tlsa {
1905 usage: 3,
1906 selector: 0,
1907 matching_type: 0,
1908 association_data: certificate.clone(),
1909 });
1910 let answer_owner_first_byte = 12 + query.question.name.wire_len() + 4 + 1;
1911 *response.get_mut(answer_owner_first_byte).unwrap() = b'x';
1912 let attempt = engine
1913 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1914 .unwrap();
1915 let parsed = engine
1916 .parse_response(&attempt, &response, ParseLimits::requester())
1917 .unwrap();
1918 assert!(matches!(
1919 engine.complete_resolution_with_local_dane(
1920 &attempt,
1921 &parsed,
1922 verified_prerequisites(),
1923 &certificate,
1924 DaneLimits::default(),
1925 CompletionContext::default(),
1926 ),
1927 Err(EngineError::Dane(hns_dane::DaneError::MissingTlsa))
1928 ));
1929
1930 let (query, response, mut wrong_certificate) = exact_certificate_exchange();
1931 let last = wrong_certificate.len() - 1;
1932 *wrong_certificate.get_mut(last).unwrap() ^= 1;
1933 let attempt = engine
1934 .admit_resolution(ResolutionTransport::DirectAuthoritativeTcp, query)
1935 .unwrap();
1936 let parsed = engine
1937 .parse_response(&attempt, &response, ParseLimits::requester())
1938 .unwrap();
1939 assert!(matches!(
1940 engine.complete_resolution_with_local_dane(
1941 &attempt,
1942 &parsed,
1943 verified_prerequisites(),
1944 &wrong_certificate,
1945 DaneLimits::default(),
1946 CompletionContext::default(),
1947 ),
1948 Err(EngineError::Dane(hns_dane::DaneError::Mismatch))
1949 ));
1950 }
1951
1952 #[test]
1953 fn policy_update_rejects_stale_response() {
1954 let engine = ready_engine();
1955 let query =
1956 Query::new(0x1234, Name::from_ascii("example").unwrap(), RecordType::A).unwrap();
1957 let attempt = engine
1958 .admit_resolution(ResolutionTransport::HandshakeP2pDnsRelay, query)
1959 .unwrap();
1960 let mut policy = engine.snapshot().unwrap().policy.config();
1961 policy.dns_relay_requester = DnsRelayRequesterPolicy::Disabled;
1962 policy.oblivious_dns = ObliviousDnsPolicy::Required;
1963 engine.update_policy(1, policy).unwrap();
1964
1965 assert!(matches!(
1966 engine.parse_response(
1967 &attempt,
1968 RESPONSE_WITH_UNTRUSTED_AD,
1969 ParseLimits::requester()
1970 ),
1971 Err(EngineError::StaleRuntimeGeneration)
1972 ));
1973 }
1974
1975 #[test]
1976 fn engine_gateway_revokes_on_policy_generation_change() {
1977 let engine = ready_engine();
1978 let before = engine.snapshot().unwrap().policy;
1979 let mut gateway = engine.begin_gateway(GatewayLimits::default()).unwrap();
1980 let attempt = gateway.next_attempt(before, 100).unwrap();
1981 assert_eq!(
1982 attempt.transport(),
1983 ResolutionTransport::DirectAuthoritativeUdp
1984 );
1985
1986 let mut next = before.config();
1987 next.authenticated_authoritative_doh = false;
1988 engine.update_policy(before.generation(), next).unwrap();
1989 let after = engine.snapshot().unwrap().policy;
1990 assert!(matches!(
1991 gateway.next_attempt(after, 101),
1992 Err(hns_gateway::GatewayError::StalePolicy)
1993 ));
1994 assert!(matches!(
1995 gateway.next_attempt(before, 101),
1996 Err(hns_gateway::GatewayError::Terminal)
1997 ));
1998 }
1999
2000 #[test]
2001 fn gateway_selection_atomically_binds_response_and_identities() {
2002 let engine = ready_engine();
2003 let (query, response, _) = exact_certificate_exchange();
2004 let selection = odoh_gateway_selection(&engine, &response).unwrap();
2005 let admitted = engine
2006 .admit_gateway_selection(selection, query, ParseLimits::requester())
2007 .unwrap();
2008 assert_eq!(
2009 admitted.attempt().transport(),
2010 ResolutionTransport::HandshakeP2pOdoh
2011 );
2012 assert_eq!(
2013 admitted.context().proxy_identity.as_deref(),
2014 Some("brontide:proxy")
2015 );
2016 assert_eq!(
2017 admitted.context().target_identity.as_deref(),
2018 Some("brontide:target")
2019 );
2020 assert_eq!(admitted.response().message().header.id, 0x1234);
2021 }
2022
2023 #[test]
2024 fn stale_gateway_selection_consumes_no_engine_event() {
2025 let engine = ready_engine();
2026 let (query, response, _) = exact_certificate_exchange();
2027 let selection = odoh_gateway_selection(&engine, &response).unwrap();
2028 let before = engine.snapshot().unwrap();
2029 let mut next = before.policy.config();
2030 next.authenticated_authoritative_doh = !next.authenticated_authoritative_doh;
2031 engine
2032 .update_policy(before.policy.generation(), next)
2033 .unwrap();
2034 let after_update = engine.snapshot().unwrap();
2035 assert!(matches!(
2036 engine.admit_gateway_selection(selection, query, ParseLimits::requester()),
2037 Err(EngineError::StaleGatewaySelection)
2038 ));
2039 assert_eq!(
2040 engine.snapshot().unwrap().event_sequence,
2041 after_update.event_sequence
2042 );
2043 }
2044
2045 #[test]
2046 fn odoh_completion_requires_distinct_bounded_identities() {
2047 let engine = ready_engine();
2048 let (query, response, certificate) = exact_certificate_exchange();
2049 let attempt = engine
2050 .admit_resolution(ResolutionTransport::HandshakeP2pOdoh, query)
2051 .unwrap();
2052 let parsed = engine
2053 .parse_response(&attempt, &response, ParseLimits::requester())
2054 .unwrap();
2055
2056 assert!(matches!(
2057 engine.complete_resolution_with_local_dane(
2058 &attempt,
2059 &parsed,
2060 verified_prerequisites(),
2061 &certificate,
2062 DaneLimits::default(),
2063 CompletionContext::default()
2064 ),
2065 Err(EngineError::MissingTransportIdentity)
2066 ));
2067 assert!(matches!(
2068 engine.complete_resolution_with_local_dane(
2069 &attempt,
2070 &parsed,
2071 verified_prerequisites(),
2072 &certificate,
2073 DaneLimits::default(),
2074 CompletionContext {
2075 proxy_identity: Some("same-peer".to_owned()),
2076 target_identity: Some("same-peer".to_owned()),
2077 ..CompletionContext::default()
2078 }
2079 ),
2080 Err(EngineError::ProxyTargetNotSeparated)
2081 ));
2082 assert!(matches!(
2083 engine.complete_resolution_with_local_dane(
2084 &attempt,
2085 &parsed,
2086 verified_prerequisites(),
2087 &certificate,
2088 DaneLimits::default(),
2089 CompletionContext {
2090 proxy_identity: Some("p".repeat(MAX_TRANSPORT_IDENTITY_BYTES + 1)),
2091 target_identity: Some("target".to_owned()),
2092 ..CompletionContext::default()
2093 }
2094 ),
2095 Err(EngineError::InvalidTransportIdentity)
2096 ));
2097 }
2098
2099 #[test]
2100 fn completion_identity_topology_matches_observability() {
2101 assert!(matches!(
2102 validate_completion_context(
2103 ResolutionTransport::HandshakeP2pOdoh,
2104 &CompletionContext {
2105 peer_identity: Some("extra-peer".to_owned()),
2106 proxy_identity: Some("proxy".to_owned()),
2107 target_identity: Some("target".to_owned()),
2108 ..CompletionContext::default()
2109 }
2110 ),
2111 Err(EngineError::InvalidCompletionContext)
2112 ));
2113 assert!(matches!(
2114 validate_completion_context(
2115 ResolutionTransport::HandshakeP2pDnsRelay,
2116 &CompletionContext {
2117 peer_identity: Some("relay".to_owned()),
2118 proxy_identity: Some("extra-proxy".to_owned()),
2119 ..CompletionContext::default()
2120 }
2121 ),
2122 Err(EngineError::InvalidCompletionContext)
2123 ));
2124 assert!(matches!(
2125 validate_completion_context(
2126 ResolutionTransport::DirectAuthoritativeTcp,
2127 &CompletionContext {
2128 peer_identity: Some("extra-peer".to_owned()),
2129 ..CompletionContext::default()
2130 }
2131 ),
2132 Err(EngineError::InvalidCompletionContext)
2133 ));
2134 }
2135
2136 #[test]
2137 fn persisted_policy_survives_restart() {
2138 let engine = Engine::new(EngineConfig::new(
2139 RuntimeSessionId::new([8; 16]).unwrap(),
2140 Network::Mainnet,
2141 PolicySnapshot::default(),
2142 ));
2143 let mut policy = engine.snapshot().unwrap().policy.config();
2144 policy.dns_relay_requester = DnsRelayRequesterPolicy::Disabled;
2145 engine.update_policy(1, policy).unwrap();
2146 let blob = engine.export_policy().unwrap();
2147
2148 let reopened = Engine::from_persisted([9; 16], Network::Testnet, &blob).unwrap();
2149 assert_eq!(
2150 reopened.snapshot().unwrap().policy,
2151 engine.snapshot().unwrap().policy
2152 );
2153 assert!(
2154 !reopened
2155 .transport_plan()
2156 .unwrap()
2157 .contains(ResolutionTransport::HandshakeP2pDnsRelay)
2158 );
2159 }
2160
2161 #[test]
2162 fn persisted_engine_rejects_zero_runtime_session() {
2163 let policy = PolicySnapshot::default().encode();
2164 assert!(matches!(
2165 Engine::from_persisted([0; 16], Network::Mainnet, &policy),
2166 Err(EngineError::InvalidRuntimeSession)
2167 ));
2168 }
2169}