Skip to main content

hns_dane_engine/
lib.rs

1//! Runtime-independent HNS browser engine facade.
2//!
3//! Native adapters supply transport bytes, a presented leaf certificate, and
4//! prerequisite local cryptographic verdicts. The engine supplies
5//! deterministic state, query correlation, local DANE-EE matching, policy
6//! generation revocation, and structured provenance.
7
8#![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
41/// Stable Rust facade API version.
42pub const ENGINE_API_VERSION: u32 = 2;
43/// Maximum UTF-8 bytes accepted for one transport identity.
44pub const MAX_TRANSPORT_IDENTITY_BYTES: usize = 256;
45
46/// Engine construction configuration.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct EngineConfig {
49    /// Checked caller-generated runtime session ID.
50    pub runtime_session: RuntimeSessionId,
51    /// Handshake network.
52    pub network: Network,
53    /// Persisted policy snapshot.
54    pub policy: PolicySnapshot,
55}
56
57impl EngineConfig {
58    /// Construct a configuration from an already checked runtime session.
59    #[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/// Immutable structured engine status.
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct EngineSnapshot {
76    /// Facade schema version.
77    pub schema_version: u16,
78    /// Runtime session ID.
79    pub runtime_session: [u8; 16],
80    /// Runtime generation.
81    pub runtime_generation: u64,
82    /// Monotonic event sequence.
83    pub event_sequence: u64,
84    /// Handshake network.
85    pub network: Network,
86    /// Authority state.
87    pub authority_state: AuthorityState,
88    /// Persistent policy.
89    pub policy: PolicySnapshot,
90}
91
92/// Runtime-owned fields needed to produce shared status.
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct ObservabilityRuntime {
95    /// Exact canonical experimental registry fingerprint.
96    pub registry_fingerprint: [u8; 32],
97    /// Negotiated protocol/status version.
98    pub protocol_version: u16,
99    /// Provider readiness after socket/storage admission.
100    pub provider_readiness: ProviderReadiness,
101    /// Name-free aggregate rate-limit status.
102    pub rate_limits: RateLimitState,
103    /// Full-host dual-root outcome kind, without names or plans.
104    pub namespace_outcome: Option<OutcomeKind>,
105    /// Name-free HNS root lookup failure.
106    pub hns_root_failure: Option<RootFailureKind>,
107    /// Name-free ICANN root lookup failure.
108    pub icann_root_failure: Option<RootFailureKind>,
109    /// Namespace selected for the current decision.
110    pub selected_namespace: Option<Namespace>,
111    /// Stable namespace-selection reason.
112    pub selection_reason: Option<SelectionReason>,
113    /// Name-free namespace decision fingerprint.
114    pub decision_fingerprint: Option<[u8; 32]>,
115    /// Current ICANN DANE/WebPKI/fail-closed action.
116    ///
117    /// This may be absent for an intentionally cleartext scheme.
118    pub icann_tls_action: Option<IcannTlsAction>,
119    /// Canonical validating-DoH DNSSEC disposition for the ICANN action.
120    pub icann_dnssec_status: Option<IcannDnssecStatus>,
121    /// ICANN validating-DoH evidence for a selected plan or failed lookup.
122    ///
123    /// This is required when `selected_namespace` is ICANN or
124    /// `icann_root_failure` is present. Secondary-root evidence does not
125    /// belong in a successful selected-plan status.
126    pub icann_evidence: Option<ValidationEvidence>,
127    /// Recoverable degraded reason.
128    pub degraded_reason: Option<DegradedReason>,
129    /// Revocation reason.
130    pub revocation_reason: Option<RevocationReason>,
131    /// Bounded unsupported evidence details.
132    pub unsupported_evidence: Vec<UnsupportedEvidence>,
133}
134
135impl ObservabilityRuntime {
136    /// Construct status inputs whose provider readiness is derived from policy.
137    #[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/// A query and transport admission bound to engine generations.
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct ResolutionAttempt {
169    runtime_stamp: RuntimeStamp,
170    admission: Admission,
171    query: Query,
172}
173
174impl ResolutionAttempt {
175    /// Runtime session at admission.
176    #[must_use]
177    pub const fn runtime_session(&self) -> [u8; 16] {
178        self.runtime_stamp.session()
179    }
180
181    /// Runtime generation at admission.
182    #[must_use]
183    pub const fn runtime_generation(&self) -> u64 {
184        self.runtime_stamp.generation()
185    }
186
187    /// Policy generation at admission.
188    #[must_use]
189    pub const fn policy_generation(&self) -> u64 {
190        self.admission.policy_generation
191    }
192
193    /// Actual transport for this attempt.
194    #[must_use]
195    pub const fn transport(&self) -> ResolutionTransport {
196        self.admission.transport
197    }
198
199    /// Correlated query.
200    #[must_use]
201    pub const fn query(&self) -> &Query {
202        &self.query
203    }
204}
205
206/// Owned, structurally correlated DNS response.
207#[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    /// Parsed response message.
216    #[must_use]
217    pub const fn message(&self) -> &Message {
218        &self.message
219    }
220
221    /// Remote AD assertion. This is never local validation evidence.
222    #[must_use]
223    pub const fn untrusted_ad_claim(&self) -> bool {
224        self.untrusted_ad_claim
225    }
226}
227
228/// Gateway-selected response atomically admitted to the current engine.
229#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct GatewayResolution {
231    attempt: ResolutionAttempt,
232    response: ParsedResponse,
233    context: CompletionContext,
234}
235
236impl GatewayResolution {
237    /// Current-generation resolution attempt.
238    #[must_use]
239    pub const fn attempt(&self) -> &ResolutionAttempt {
240        &self.attempt
241    }
242
243    /// Locally parsed and correlated gateway response.
244    #[must_use]
245    pub const fn response(&self) -> &ParsedResponse {
246        &self.response
247    }
248
249    /// Gateway-derived intermediary identities and downgrade state.
250    #[must_use]
251    pub const fn context(&self) -> &CompletionContext {
252        &self.context
253    }
254
255    /// Consume into the three inputs used by local DNSSEC/DANE completion.
256    #[must_use]
257    pub fn into_parts(self) -> (ResolutionAttempt, ParsedResponse, CompletionContext) {
258        (self.attempt, self.response, self.context)
259    }
260}
261
262/// Optional identities and chain anchor attached to a completed resolution.
263#[derive(Clone, Debug, Default, Eq, PartialEq)]
264pub struct CompletionContext {
265    /// Locally validated chain anchor.
266    pub chain_anchor: Option<ChainAnchor>,
267    /// Relay or P2P peer identity.
268    pub peer_identity: Option<String>,
269    /// ODoH proxy identity.
270    pub proxy_identity: Option<String>,
271    /// ODoH target identity.
272    pub target_identity: Option<String>,
273    /// Whether ODoH privacy downgraded to direct relay.
274    pub direct_relay_fallback: bool,
275}
276
277/// Local evidence required before the engine performs TLSA/DANE matching.
278///
279/// TLSA and DANE fields are deliberately absent: the engine derives them from
280/// the correlated response and presented certificate.
281#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282pub struct LocalDanePrerequisites {
283    /// Verified Handshake state and Urkel proof.
284    pub hns_proof: EvidenceState,
285    /// Locally verified DNSSEC chain covering the correlated TLSA RRset.
286    pub dnssec: EvidenceState,
287    /// Chain currency sufficiency.
288    pub chain_current: EvidenceState,
289    /// Origin SNI match for the presented TLS certificate.
290    pub origin_sni: EvidenceState,
291}
292
293/// Inputs for engine-owned DNSSEC and DANE completion.
294#[derive(Clone, Copy, Debug)]
295pub struct ValidatedDaneInput<'a> {
296    /// Non-forgeable resolver result carrying HNS header-to-DNSSEC lineage.
297    pub validated: &'a ValidatedTlsa,
298    /// TLS server chain, leaf first.
299    pub certificate_chain_der: &'a [&'a [u8]],
300    /// Exact SNI sent to the TLS origin.
301    pub origin_sni: &'a str,
302    /// Explicit certificate validation time for DANE-TA.
303    pub validation_unix_time: i64,
304    /// DANE resource bounds.
305    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/// Completed provenance plus the locally matched TLSA record details.
318#[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    /// Fully verified HNS HTTPS provenance.
329    #[must_use]
330    pub const fn provenance(&self) -> &ResolutionProvenance {
331        &self.provenance
332    }
333
334    /// Match derived locally from the correlated TLSA answer and certificate.
335    #[must_use]
336    pub const fn dane_match(&self) -> DaneMatch {
337        self.dane_match
338    }
339
340    /// Exact strict-path origin, absent for the legacy prerequisite API.
341    #[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/// Non-forgeable current-generation permission for an exact browser origin.
364#[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    /// Runtime session that issued the authorization.
377    #[must_use]
378    pub const fn runtime_session(&self) -> [u8; 16] {
379        self.runtime_session
380    }
381
382    /// Runtime generation that issued the authorization.
383    #[must_use]
384    pub const fn runtime_generation(&self) -> u64 {
385        self.runtime_generation
386    }
387
388    /// Policy generation that issued the authorization.
389    #[must_use]
390    pub const fn policy_generation(&self) -> u64 {
391        self.policy_generation
392    }
393
394    /// Authorization event sequence.
395    #[must_use]
396    pub const fn event_sequence(&self) -> u64 {
397        self.event_sequence
398    }
399
400    /// First chain-currency time at which this grant may be consumed.
401    #[must_use]
402    pub const fn valid_from(&self) -> u64 {
403        self.valid_from
404    }
405
406    /// Last chain-currency time at which this grant may be consumed.
407    #[must_use]
408    pub const fn valid_until(&self) -> u64 {
409        self.valid_until
410    }
411
412    /// Exact normalized origin SNI.
413    #[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/// Thread-safe deterministic browser engine.
444#[derive(Debug)]
445pub struct Engine {
446    state: RwLock<EngineState>,
447}
448
449impl Engine {
450    /// Create an engine from a checked configuration.
451    #[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    /// Create from a versioned persisted policy blob.
465    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    /// Read structured status.
480    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    /// Export the exact persistent policy representation.
495    pub fn export_policy(&self) -> Result<[u8; 32], EngineError> {
496        Ok(self.snapshot()?.policy.encode())
497    }
498
499    /// Read the direct-authoritative-first current plan.
500    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    /// Begin one bounded fail-closed transport gateway under current policy.
506    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    /// Produce the complete, bounded shared browser status.
512    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    /// Replace policy and increment runtime generation when it changes.
607    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    /// Replace policy from a persistence blob whose generation must match.
630    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    /// Advance the explicit authority state machine.
643    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    /// Admit one exact query on one current transport.
665    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    /// Parse and correlate transport bytes, rejecting revoked generations.
684    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    /// Atomically admit a gateway selection under the current policy/runtime.
704    ///
705    /// Response bytes are parsed and exactly correlated before an engine event
706    /// is consumed. The selection's policy generation, selected transport,
707    /// identities, and privacy-downgrade state are then admitted together
708    /// under one write lock, so callers cannot substitute completion context.
709    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    /// Complete a TLSA response after matching its RRset to the leaf certificate.
756    ///
757    /// The query must be an exact class-IN TLSA query. Only same-owner TLSA
758    /// answers from the already correlated response are considered; CNAME
759    /// chasing and fallback trust paths are intentionally absent.
760    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    /// Complete from non-forgeable local DNSSEC/TLSA evidence.
804    ///
805    /// The terminal response must carry the exact RRset represented by
806    /// `validated`. The supplied origin SNI must equal the original TLSA base
807    /// domain. DANE-EE or DANE-TA matching is then performed locally with no
808    /// WebPKI fallback.
809    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    /// Authorize the exact strict-path origin for the browser bridge.
882    ///
883    /// The completion must still be this engine's latest current-generation
884    /// provenance. Legacy caller-prerequisite completions cannot mint a bridge
885    /// authorization because they carry no engine-verified origin binding.
886    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/// Facade failure.
1126#[derive(Debug)]
1127#[non_exhaustive]
1128pub enum EngineError {
1129    /// Runtime session uses the forbidden all-zero sentinel.
1130    InvalidRuntimeSession,
1131    /// Internal lock was poisoned by a caller panic.
1132    LockPoisoned,
1133    /// Runtime or event generation cannot advance.
1134    GenerationExhausted,
1135    /// Authority state transition is invalid.
1136    InvalidAuthorityTransition,
1137    /// Local authority prerequisites are not ready.
1138    AuthorityNotReady,
1139    /// Work belongs to an older runtime generation.
1140    StaleRuntimeGeneration,
1141    /// Gateway selection belongs to an older policy generation.
1142    StaleGatewaySelection,
1143    /// Parsed response belongs to another attempt.
1144    ResponseAttemptMismatch,
1145    /// A TLSA response carried a nonzero DNS response code.
1146    UnsuccessfulDnsResponse,
1147    /// Completion was attempted for a query other than class-IN TLSA.
1148    ExpectedTlsaQuery,
1149    /// An intermediary path omitted its required identity.
1150    MissingTransportIdentity,
1151    /// An intermediary identity is empty or exceeds its bound.
1152    InvalidTransportIdentity,
1153    /// ODoH proxy and target identities are not distinct.
1154    ProxyTargetNotSeparated,
1155    /// Completion context conflicts with the admitted transport.
1156    InvalidCompletionContext,
1157    /// Locally validated TLSA evidence does not match the terminal response.
1158    ResponseEvidenceMismatch,
1159    /// Actual origin SNI differs from the original TLSA base domain.
1160    OriginSniMismatch,
1161    /// Resolver evidence was not rooted in a current verified HNS resource.
1162    MissingHnsAuthority,
1163    /// DNSSEC/certificate time differs from the HNS authority validation time.
1164    ValidationTimeMismatch,
1165    /// Resolver evidence belongs to another Handshake network.
1166    HnsNetworkMismatch,
1167    /// Caller-supplied provenance conflicts with the derived HNS anchor.
1168    ChainAnchorMismatch,
1169    /// Legacy prerequisite completion has no engine-verified origin binding.
1170    LegacyCompletionNotBridgeable,
1171    /// Completion is no longer the engine's latest current-generation result.
1172    CompletionNotCurrent,
1173    /// Completion's chain-currency validity window elapsed.
1174    CompletionExpired,
1175    /// Completion predates the beginning of its chain-currency validity window.
1176    CompletionNotYetValid,
1177    /// ICANN selection or root failure omitted explicit validation evidence.
1178    MissingIcannEvidence,
1179    /// ICANN evidence was supplied without ICANN selection or root failure.
1180    UnexpectedIcannEvidence,
1181    /// DNS wire failure.
1182    Wire(hns_dns_wire::Error),
1183    /// Local TLSA/DANE matching failure.
1184    Dane(hns_dane::DaneError),
1185    /// Policy failure.
1186    Policy(PolicyError),
1187    /// Shared observability snapshot failed invariant checks.
1188    Status(StatusError),
1189    /// Fail-closed transport gateway rejected its bounds or lifecycle.
1190    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}