Skip to main content

ans_verify/
verify.rs

1//! Verification logic for ANS trust verification.
2//!
3//! This module provides:
4//! - `ServerVerifier`: For clients verifying servers
5//! - `ClientVerifier`: For servers verifying mTLS clients
6//! - `AnsVerifier`: High-level facade combining both
7
8use std::collections::HashSet;
9use std::fmt;
10use std::sync::Arc;
11use std::time::Duration;
12
13use futures_util::future::join_all;
14
15use crate::cache::{BadgeCache, CacheConfig, CacheKey};
16use crate::dane::{DanePolicy, DaneVerificationResult, verify_dane};
17use crate::dns::{
18    BadgeRecord, DnsLookupResult, DnsResolver, DnsResolverConfig, HickoryDnsResolver,
19};
20use crate::error::{AnsError, AnsResult, DaneError, DnsError, TlogError, VerificationError};
21use crate::tlog::{HttpTransparencyLogClient, TransparencyLogClient};
22use ans_types::{AnsName, Badge, BadgeStatus, CertFingerprint, CryptoError, Fqdn, Version};
23
24/// Parsed certificate data: (Common Name, DNS SANs, URI SANs).
25type ParsedCertData = (Option<String>, Vec<String>, Vec<String>);
26
27/// Extracted identity information from a certificate.
28///
29/// This struct holds the relevant identity information extracted from
30/// an X.509 certificate for ANS verification purposes.
31///
32/// In production, construct via [`CertIdentity::from_der`]. The [`CertIdentity::new`]
33/// and [`CertIdentity::from_fingerprint_and_cn`] constructors are available only
34/// when the `test-support` feature is enabled.
35#[derive(Debug, Clone)]
36pub struct CertIdentity {
37    /// Common Name (CN) from the certificate subject.
38    pub(crate) common_name: Option<String>,
39    /// DNS Subject Alternative Names.
40    pub(crate) dns_sans: Vec<String>,
41    /// URI Subject Alternative Names.
42    pub(crate) uri_sans: Vec<String>,
43    /// Certificate fingerprint.
44    pub(crate) fingerprint: CertFingerprint,
45}
46
47impl CertIdentity {
48    /// Returns the Common Name (CN) from the certificate subject.
49    pub fn common_name(&self) -> Option<&str> {
50        self.common_name.as_deref()
51    }
52
53    /// Returns the DNS Subject Alternative Names.
54    pub fn dns_sans(&self) -> &[String] {
55        &self.dns_sans
56    }
57
58    /// Returns the URI Subject Alternative Names.
59    pub fn uri_sans(&self) -> &[String] {
60        &self.uri_sans
61    }
62
63    /// Returns the certificate fingerprint.
64    pub fn fingerprint(&self) -> &CertFingerprint {
65        &self.fingerprint
66    }
67
68    /// Create a new `CertIdentity` from components.
69    ///
70    /// Use this when you've already extracted the certificate information
71    /// using your TLS library (e.g., rustls, native-tls, openssl).
72    /// If you have DER-encoded bytes, prefer [`CertIdentity::from_der`].
73    pub fn new(
74        common_name: Option<String>,
75        dns_sans: Vec<String>,
76        uri_sans: Vec<String>,
77        fingerprint: CertFingerprint,
78    ) -> Self {
79        Self {
80            common_name,
81            dns_sans,
82            uri_sans,
83            fingerprint,
84        }
85    }
86
87    /// Create from DER-encoded certificate bytes.
88    ///
89    /// Computes the SHA-256 fingerprint and extracts the Subject CN and
90    /// Subject Alternative Names (DNS, URI) using x509-parser.
91    pub fn from_der(der: &[u8]) -> Result<Self, CryptoError> {
92        let fingerprint = CertFingerprint::from_der(der);
93        let (common_name, dns_sans, uri_sans) = Self::parse_cert_der(der)?;
94
95        Ok(Self {
96            common_name,
97            dns_sans,
98            uri_sans,
99            fingerprint,
100        })
101    }
102
103    /// Create from fingerprint and CN only.
104    ///
105    /// Sets `dns_sans` to `[cn]` and `uri_sans` to empty.
106    /// If you have DER-encoded bytes, prefer [`CertIdentity::from_der`].
107    pub fn from_fingerprint_and_cn(fingerprint: CertFingerprint, cn: String) -> Self {
108        Self {
109            common_name: Some(cn.clone()),
110            dns_sans: vec![cn],
111            uri_sans: vec![],
112            fingerprint,
113        }
114    }
115
116    /// Parse DER certificate to extract CN and SANs using x509-parser.
117    fn parse_cert_der(der: &[u8]) -> Result<ParsedCertData, CryptoError> {
118        use x509_parser::prelude::*;
119
120        let (_, cert) = X509Certificate::from_der(der)
121            .map_err(|e| CryptoError::ParseFailed(format!("X.509 parse error: {e}")))?;
122
123        // Extract Subject CN
124        let cn = cert
125            .subject()
126            .iter_common_name()
127            .next()
128            .and_then(|attr| attr.as_str().ok())
129            .map(String::from);
130
131        // Extract SANs from the SubjectAlternativeName extension
132        let mut dns_sans = Vec::new();
133        let mut uri_sans = Vec::new();
134
135        if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
136            for name in &san_ext.value.general_names {
137                match name {
138                    GeneralName::DNSName(dns) => dns_sans.push((*dns).to_string()),
139                    GeneralName::URI(uri) => uri_sans.push((*uri).to_string()),
140                    _ => {}
141                }
142            }
143        }
144
145        Ok((cn, dns_sans, uri_sans))
146    }
147
148    /// Get the FQDN from the certificate.
149    ///
150    /// Prefers DNS SAN over CN, following RFC 6125 recommendations.
151    pub fn fqdn(&self) -> Option<&str> {
152        self.dns_sans
153            .first()
154            .map(std::string::String::as_str)
155            .or(self.common_name.as_deref())
156    }
157
158    /// Get the ANS name from URI SANs.
159    pub fn ans_name(&self) -> Option<AnsName> {
160        self.uri_sans
161            .iter()
162            .filter(|uri| uri.starts_with("ans://"))
163            .find_map(|uri| AnsName::parse(uri).ok())
164    }
165
166    /// Extract version from ANS name in URI SAN.
167    pub fn version(&self) -> Option<Version> {
168        self.ans_name().map(|name| name.version().clone())
169    }
170}
171
172/// Result of a verification operation.
173#[derive(Debug)]
174#[non_exhaustive]
175pub enum VerificationOutcome {
176    /// Verification passed.
177    Verified {
178        /// The verified badge.
179        badge: Badge,
180        /// The fingerprint that matched.
181        matched_fingerprint: CertFingerprint,
182    },
183
184    /// Not an ANS agent (no badge DNS record found).
185    NotAnsAgent {
186        /// The FQDN that was looked up.
187        fqdn: String,
188    },
189
190    /// Badge status is invalid for connections.
191    InvalidStatus {
192        /// The invalid status.
193        status: BadgeStatus,
194        /// The badge with invalid status.
195        badge: Badge,
196    },
197
198    /// Certificate fingerprint does not match badge.
199    FingerprintMismatch {
200        /// Expected fingerprint from badge.
201        expected: String,
202        /// Actual fingerprint from certificate.
203        actual: String,
204        /// The badge that didn't match.
205        badge: Badge,
206    },
207
208    /// Hostname does not match badge.
209    HostnameMismatch {
210        /// Expected hostname from badge.
211        expected: String,
212        /// Actual hostname from certificate.
213        actual: String,
214        /// The badge that didn't match.
215        badge: Badge,
216    },
217
218    /// ANS name does not match badge (mTLS client verification).
219    AnsNameMismatch {
220        /// Expected ANS name from badge.
221        expected: String,
222        /// Actual ANS name from certificate.
223        actual: String,
224        /// The badge that didn't match.
225        badge: Badge,
226    },
227
228    /// Verification failed due to a DNS error.
229    DnsError(DnsError),
230
231    /// Verification failed due to a transparency log error.
232    TlogError(TlogError),
233
234    /// Verification failed due to a certificate error.
235    CertError(CryptoError),
236
237    /// Verification failed due to a parse error.
238    ParseError(ans_types::ParseError),
239
240    /// Verification failed due to a DANE/TLSA error.
241    DaneError(DaneError),
242}
243
244impl VerificationOutcome {
245    /// Check if verification was successful.
246    pub fn is_success(&self) -> bool {
247        matches!(self, Self::Verified { .. })
248    }
249
250    /// Check if the agent is not registered with ANS.
251    pub fn is_not_ans_agent(&self) -> bool {
252        matches!(self, Self::NotAnsAgent { .. })
253    }
254
255    /// Get the badge if verification succeeded or partially completed.
256    pub fn badge(&self) -> Option<&Badge> {
257        match self {
258            Self::Verified { badge, .. }
259            | Self::InvalidStatus { badge, .. }
260            | Self::FingerprintMismatch { badge, .. }
261            | Self::HostnameMismatch { badge, .. }
262            | Self::AnsNameMismatch { badge, .. } => Some(badge),
263            _ => None,
264        }
265    }
266
267    /// Convert to a Result.
268    pub fn into_result(self) -> AnsResult<Badge> {
269        match self {
270            Self::Verified { badge, .. } => Ok(badge),
271            Self::NotAnsAgent { fqdn } => Err(AnsError::Dns(DnsError::NotFound { fqdn })),
272            Self::InvalidStatus { status, .. } => {
273                Err(AnsError::Verification(VerificationError::InvalidStatus {
274                    status,
275                }))
276            }
277            Self::FingerprintMismatch {
278                expected, actual, ..
279            } => Err(AnsError::Verification(
280                VerificationError::FingerprintMismatch { expected, actual },
281            )),
282            Self::HostnameMismatch {
283                expected, actual, ..
284            } => Err(AnsError::Verification(
285                VerificationError::HostnameMismatch { expected, actual },
286            )),
287            Self::AnsNameMismatch {
288                expected, actual, ..
289            } => Err(AnsError::Verification(VerificationError::AnsNameMismatch {
290                expected,
291                actual,
292            })),
293            Self::DnsError(e) => Err(AnsError::Dns(e)),
294            Self::TlogError(e) => Err(AnsError::TransparencyLog(e)),
295            Self::CertError(e) => Err(AnsError::Certificate(e)),
296            Self::ParseError(e) => Err(AnsError::Parse(e)),
297            Self::DaneError(e) => Err(AnsError::Verification(
298                VerificationError::DaneVerificationFailed(e),
299            )),
300        }
301    }
302}
303
304/// Failure handling policy.
305#[derive(Debug, Clone, Copy, Default)]
306#[non_exhaustive]
307pub enum FailurePolicy {
308    /// Reject on any failure (most secure).
309    #[default]
310    FailClosed,
311
312    /// Use cached badge if available, otherwise reject.
313    FailOpenWithCache {
314        /// Maximum age of cached badge to accept.
315        max_staleness: Duration,
316    },
317}
318
319/// Validate that a badge URL's domain is in the trusted RA domains set.
320///
321/// Returns `Ok(())` if:
322/// - `trusted` is `None` (no restriction configured — allow all domains)
323/// - The URL's host is present in the trusted set
324///
325/// Returns `Err(TlogError::UntrustedDomain)` if the host is not trusted.
326fn validate_badge_domain(trusted: Option<&HashSet<String>>, url: &str) -> Result<(), TlogError> {
327    let Some(trusted) = trusted else {
328        return Ok(());
329    };
330    let parsed = url::Url::parse(url)
331        .map_err(|e| TlogError::InvalidUrl(format!("Badge URL is invalid: {e}")))?;
332    let domain = parsed
333        .host_str()
334        .ok_or_else(|| TlogError::InvalidUrl(format!("Badge URL has no host: {url}")))?;
335    if trusted.contains(domain) {
336        Ok(())
337    } else {
338        Err(TlogError::UntrustedDomain {
339            domain: domain.to_string(),
340            trusted: trusted.iter().cloned().collect(),
341        })
342    }
343}
344
345/// Server verifier for clients verifying agent servers.
346pub struct ServerVerifier {
347    dns_resolver: Arc<dyn DnsResolver>,
348    tlog_client: Arc<dyn TransparencyLogClient>,
349    cache: Option<Arc<BadgeCache>>,
350    failure_policy: FailurePolicy,
351    dane_policy: DanePolicy,
352    /// Port to use for TLSA lookups (default: 443).
353    dane_port: u16,
354    /// Optional set of trusted RA domains for badge URL validation.
355    trusted_ra_domains: Option<HashSet<String>>,
356}
357
358impl fmt::Debug for ServerVerifier {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("ServerVerifier")
361            .field("failure_policy", &self.failure_policy)
362            .field("dane_policy", &self.dane_policy)
363            .field("dane_port", &self.dane_port)
364            .field("has_cache", &self.cache.is_some())
365            .field("has_trusted_ra_domains", &self.trusted_ra_domains.is_some())
366            .finish_non_exhaustive()
367    }
368}
369
370impl ServerVerifier {
371    /// Create a new builder.
372    pub fn builder() -> ServerVerifierBuilder {
373        ServerVerifierBuilder::default()
374    }
375
376    /// Verify an agent server.
377    ///
378    /// # Steps
379    /// 1. DNS lookup for `_ans-badge` TXT record (with `_ra-badge` fallback)
380    /// 2. Fetch preferred badge from transparency log (newest ACTIVE first)
381    /// 3. Validate badge status
382    /// 4. Compare certificate fingerprint to badge
383    /// 5. On mismatch with multiple records, try all badges by fingerprint
384    ///    (handles multi-version transitions where both versions are ACTIVE)
385    /// 6. If still no match, refresh-on-mismatch (handles cert renewal)
386    /// 7. Compare certificate CN to badge agent.host
387    pub async fn verify(&self, fqdn: &Fqdn, server_cert: &CertIdentity) -> VerificationOutcome {
388        tracing::info!(fqdn = %fqdn, "Starting server verification");
389        tracing::debug!(
390            cert_cn = ?server_cert.common_name,
391            cert_fingerprint = %server_cert.fingerprint,
392            "Certificate details"
393        );
394
395        // Check cache first — scan all versioned badges by fingerprint
396        if let Some(cache) = &self.cache {
397            let cached_badges = cache.get_all_for_fqdn(fqdn).await;
398            if !cached_badges.is_empty() {
399                tracing::debug!(fqdn = %fqdn, count = cached_badges.len(), "Scanning cached badges");
400                for cached in &cached_badges {
401                    let outcome = self.verify_against_badge(&cached.badge, server_cert, true);
402                    if outcome.is_success() {
403                        tracing::debug!(fqdn = %fqdn, "Cache hit — badge matched");
404                        return outcome;
405                    }
406                }
407                // No cached badge matched — fall through to DNS+TLog
408                tracing::info!(fqdn = %fqdn, "No cached badge matched fingerprint, fetching fresh");
409            }
410        }
411
412        // DNS lookup
413        tracing::debug!(fqdn = %fqdn, "Performing DNS lookup for _ans-badge / _ra-badge");
414        let records = match self.dns_resolver.lookup_badge(fqdn).await {
415            Ok(DnsLookupResult::Found(records)) => {
416                tracing::debug!(count = records.len(), "Found badge records");
417                for (i, r) in records.iter().enumerate() {
418                    tracing::debug!(index = i, version = ?r.version, url = %r.url, "Badge record");
419                }
420                records
421            }
422            Ok(DnsLookupResult::NotFound) => {
423                tracing::warn!(fqdn = %fqdn, "No badge record found - not an ANS agent");
424                return VerificationOutcome::NotAnsAgent {
425                    fqdn: fqdn.to_string(),
426                };
427            }
428            Err(e) => {
429                tracing::error!(fqdn = %fqdn, error = %e, "DNS lookup failed");
430                return self.handle_dns_error(e, fqdn, server_cert).await;
431            }
432        };
433
434        // Server certs don't contain version info. Try all badge records by
435        // fingerprint to handle multi-version transitions where both versions
436        // are ACTIVE (see AGENT_TO_AGENT_FLOW §5.3).
437        let outcome = self
438            .verify_against_records(&records, fqdn, server_cert)
439            .await;
440
441        if !outcome.is_success() {
442            return outcome;
443        }
444
445        // Perform DANE verification if enabled
446        if self.dane_policy.should_verify() {
447            match self.verify_dane(fqdn, server_cert).await {
448                Ok(result) => {
449                    if !result.is_acceptable(self.dane_policy) {
450                        tracing::error!(
451                            fqdn = %fqdn,
452                            dane_policy = ?self.dane_policy,
453                            "DANE verification failed"
454                        );
455                        return VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
456                    }
457                }
458                Err(e) => {
459                    tracing::error!(fqdn = %fqdn, error = %e, "DANE verification error");
460                    return VerificationOutcome::DaneError(e);
461                }
462            }
463        }
464
465        outcome
466    }
467
468    /// Try all badge records to find one matching the server certificate.
469    ///
470    /// Server certificates don't contain version info, so during multi-version
471    /// transitions (where multiple versions are ACTIVE), we must try each badge
472    /// by fingerprint comparison. Prefers newest ACTIVE badge, falls back to
473    /// older versions and AHP-deprecated badges.
474    ///
475    /// If no badge matches from any record, falls back to refresh-on-mismatch
476    /// (handles the certificate renewal case where the `TLog` was recently updated).
477    async fn verify_against_records(
478        &self,
479        records: &[BadgeRecord],
480        fqdn: &Fqdn,
481        server_cert: &CertIdentity,
482    ) -> VerificationOutcome {
483        // Sort by version descending (newest first)
484        let mut sorted: Vec<_> = records.iter().collect();
485        sorted.sort_by(|a, b| b.version.cmp(&a.version));
486
487        // Pre-populate the version index from DNS records
488        if let Some(cache) = &self.cache {
489            let versions: Vec<Version> =
490                sorted.iter().filter_map(|r| r.version().cloned()).collect();
491            if !versions.is_empty() {
492                cache.set_version_index(fqdn, versions).await;
493            }
494        }
495
496        // Fetch all badges in parallel
497        let results = self.fetch_badges_parallel(&sorted).await;
498
499        let mut last_mismatch: Option<VerificationOutcome> = None;
500        let mut last_error: Option<AnsError> = None;
501
502        for (record, result) in results {
503            let badge = match result {
504                Ok(b) => b,
505                Err(e) => {
506                    tracing::debug!(url = %record.url, error = %e, "Failed to fetch badge, trying next");
507                    last_error = Some(AnsError::TransparencyLog(e));
508                    continue;
509                }
510            };
511
512            tracing::debug!(
513                version = ?record.version,
514                status = ?badge.status,
515                "Checking badge record"
516            );
517
518            // Cache every successfully fetched badge by version
519            if let Some(cache) = &self.cache {
520                let version = record
521                    .version()
522                    .cloned()
523                    .or_else(|| badge.agent_version().parse::<Version>().ok());
524                if let Some(v) = &version {
525                    cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
526                    tracing::debug!(fqdn = %fqdn, version = %v, "Cached badge by version");
527                }
528            }
529
530            let outcome = self.verify_against_badge(&badge, server_cert, true);
531
532            match &outcome {
533                VerificationOutcome::Verified { .. } => {
534                    return outcome;
535                }
536                VerificationOutcome::FingerprintMismatch { .. } => {
537                    tracing::debug!(version = ?record.version, "Fingerprint mismatch, trying next record");
538                    last_mismatch = Some(outcome);
539                }
540                // Non-fingerprint failures (status rejected, hostname mismatch) are terminal
541                _ => return outcome,
542            }
543        }
544
545        // No badge matched by fingerprint. Try refresh-on-mismatch for the
546        // cert renewal case (TLog updated after our DNS lookup).
547        if last_mismatch.is_some() {
548            tracing::info!(fqdn = %fqdn, "No badge matched, attempting refresh-on-mismatch");
549            return self.verify_with_refresh(fqdn, server_cert).await;
550        }
551
552        // All fetches failed
553        match last_error {
554            Some(e) => self.handle_ans_error(e, fqdn, server_cert).await,
555            None => VerificationOutcome::NotAnsAgent {
556                fqdn: fqdn.to_string(),
557            },
558        }
559    }
560
561    /// Perform DANE/TLSA verification.
562    async fn verify_dane(
563        &self,
564        fqdn: &Fqdn,
565        cert: &CertIdentity,
566    ) -> Result<DaneVerificationResult, DaneError> {
567        tracing::debug!(
568            fqdn = %fqdn,
569            port = self.dane_port,
570            policy = ?self.dane_policy,
571            "Starting DANE verification"
572        );
573
574        let tlsa_records = self
575            .dns_resolver
576            .get_tlsa_records(fqdn, self.dane_port)
577            .await?;
578
579        verify_dane(
580            &tlsa_records,
581            &cert.fingerprint,
582            self.dane_policy,
583            fqdn,
584            self.dane_port,
585        )
586    }
587
588    /// Pre-fetch badges for caching (before TLS connection).
589    ///
590    /// Fetches ALL badge records from DNS, then fetches and caches each badge
591    /// by version. Returns the preferred (newest active) badge.
592    pub async fn prefetch(&self, fqdn: &Fqdn) -> Result<Badge, AnsError> {
593        let records = match self.dns_resolver.lookup_badge(fqdn).await {
594            Ok(DnsLookupResult::Found(records)) => records,
595            Ok(DnsLookupResult::NotFound) => {
596                return Err(AnsError::Dns(DnsError::NotFound {
597                    fqdn: fqdn.to_string(),
598                }));
599            }
600            Err(e) => return Err(AnsError::Dns(e)),
601        };
602
603        // Sort by version descending (newest first)
604        let mut sorted: Vec<_> = records.iter().collect();
605        sorted.sort_by(|a, b| b.version.cmp(&a.version));
606
607        // Pre-populate the version index from DNS records
608        if let Some(cache) = &self.cache {
609            let versions: Vec<Version> =
610                sorted.iter().filter_map(|r| r.version().cloned()).collect();
611            if !versions.is_empty() {
612                cache.set_version_index(fqdn, versions).await;
613            }
614        }
615
616        // Fetch ALL badges in parallel, then process results
617        let results = self.fetch_badges_parallel(&sorted).await;
618
619        let mut preferred: Option<Badge> = None;
620        let mut last_error = None;
621
622        for (record, result) in results {
623            match result {
624                Ok(badge) => {
625                    // Cache by version
626                    if let Some(cache) = &self.cache {
627                        let version = record
628                            .version()
629                            .cloned()
630                            .or_else(|| badge.agent_version().parse::<Version>().ok());
631                        if let Some(v) = &version {
632                            cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
633                            tracing::debug!(fqdn = %fqdn, version = %v, "Prefetch: cached badge");
634                        }
635                    }
636
637                    // Track preferred (first active, then deprecated, as fallback)
638                    if preferred.is_none()
639                        && (badge.status.is_active() || badge.status == BadgeStatus::Deprecated)
640                    {
641                        preferred = Some(badge);
642                    }
643                }
644                Err(e) => {
645                    last_error = Some(e);
646                }
647            }
648        }
649
650        match preferred {
651            Some(badge) => Ok(badge),
652            None => match last_error {
653                Some(e) => Err(AnsError::TransparencyLog(e)),
654                None => Err(AnsError::TransparencyLog(TlogError::InvalidResponse(
655                    "no badge records available".to_string(),
656                ))),
657            },
658        }
659    }
660
661    /// Refresh-on-mismatch: invalidate cache, re-fetch from DNS and `TLog`, re-verify.
662    ///
663    /// Called when no badge record matched by fingerprint. Handles the cert
664    /// renewal case where the `TLog` was updated after the initial fetch.
665    /// Tries all records to also handle multi-version transitions.
666    async fn verify_with_refresh(
667        &self,
668        fqdn: &Fqdn,
669        server_cert: &CertIdentity,
670    ) -> VerificationOutcome {
671        // Invalidate all cached badges for this FQDN
672        if let Some(cache) = &self.cache {
673            cache.invalidate_fqdn(fqdn).await;
674        }
675
676        // Re-fetch from DNS + tlog, trying all records
677        let records = match self.dns_resolver.lookup_badge(fqdn).await {
678            Ok(DnsLookupResult::Found(records)) => records,
679            Ok(DnsLookupResult::NotFound) => {
680                return VerificationOutcome::NotAnsAgent {
681                    fqdn: fqdn.to_string(),
682                };
683            }
684            Err(e) => return VerificationOutcome::DnsError(e),
685        };
686
687        // Try all records — this is the final answer (no further refresh)
688        self.verify_against_records_final(&records, fqdn, server_cert)
689            .await
690    }
691
692    /// Try all badge records without further refresh fallback (terminal attempt).
693    async fn verify_against_records_final(
694        &self,
695        records: &[BadgeRecord],
696        fqdn: &Fqdn,
697        server_cert: &CertIdentity,
698    ) -> VerificationOutcome {
699        let mut sorted: Vec<_> = records.iter().collect();
700        sorted.sort_by(|a, b| b.version.cmp(&a.version));
701
702        // Pre-populate the version index from DNS records
703        if let Some(cache) = &self.cache {
704            let versions: Vec<Version> =
705                sorted.iter().filter_map(|r| r.version().cloned()).collect();
706            if !versions.is_empty() {
707                cache.set_version_index(fqdn, versions).await;
708            }
709        }
710
711        // Fetch all badges in parallel
712        let results = self.fetch_badges_parallel(&sorted).await;
713
714        let mut last_mismatch: Option<VerificationOutcome> = None;
715        let mut last_error: Option<AnsError> = None;
716
717        for (record, result) in results {
718            let badge = match result {
719                Ok(b) => b,
720                Err(e) => {
721                    last_error = Some(AnsError::TransparencyLog(e));
722                    continue;
723                }
724            };
725
726            // Cache every successfully fetched badge by version
727            if let Some(cache) = &self.cache {
728                let version = record
729                    .version()
730                    .cloned()
731                    .or_else(|| badge.agent_version().parse::<Version>().ok());
732                if let Some(v) = &version {
733                    cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
734                }
735            }
736
737            let outcome = self.verify_against_badge(&badge, server_cert, true);
738
739            match &outcome {
740                VerificationOutcome::Verified { .. } => {
741                    return outcome;
742                }
743                VerificationOutcome::FingerprintMismatch { .. } => {
744                    last_mismatch = Some(outcome);
745                }
746                _ => return outcome,
747            }
748        }
749
750        // Return last mismatch or last error
751        if let Some(mismatch) = last_mismatch {
752            return mismatch;
753        }
754        match last_error {
755            Some(e) => self.handle_ans_error(e, fqdn, server_cert).await,
756            None => VerificationOutcome::NotAnsAgent {
757                fqdn: fqdn.to_string(),
758            },
759        }
760    }
761
762    /// Fetch badges from the transparency log in parallel.
763    ///
764    /// Validates badge domains first (pure check), then fires all HTTP requests
765    /// concurrently via `join_all`. Returns results paired with their records,
766    /// preserving the input ordering.
767    async fn fetch_badges_parallel<'a>(
768        &self,
769        records: &'a [&'a BadgeRecord],
770    ) -> Vec<(&'a BadgeRecord, Result<Badge, TlogError>)> {
771        // Pair each record with a future: domain-invalid records get an
772        // immediate Err, valid ones get a real TLog fetch.
773        let futures: Vec<_> = records
774            .iter()
775            .map(|record| {
776                let tlog = &self.tlog_client;
777                let trusted = &self.trusted_ra_domains;
778                async move {
779                    if let Err(e) = validate_badge_domain(trusted.as_ref(), &record.url) {
780                        (*record, Err(e))
781                    } else {
782                        let result = tlog.fetch_badge(&record.url).await;
783                        (*record, result)
784                    }
785                }
786            })
787            .collect();
788
789        join_all(futures).await
790    }
791
792    #[allow(clippy::unused_self)] // logically part of ServerVerifier; may use self in future
793    fn verify_against_badge(
794        &self,
795        badge: &Badge,
796        cert: &CertIdentity,
797        is_server: bool,
798    ) -> VerificationOutcome {
799        let cert_type = if is_server { "server" } else { "identity" };
800        tracing::debug!(cert_type, "Verifying certificate against badge");
801
802        // Check status
803        if badge.status.should_reject() {
804            tracing::warn!(
805                status = ?badge.status,
806                "Badge status is not valid for connections"
807            );
808            return VerificationOutcome::InvalidStatus {
809                status: badge.status,
810                badge: badge.clone(),
811            };
812        }
813        tracing::debug!(status = ?badge.status, "Badge status is valid");
814
815        // Compare fingerprint
816        let expected_fp = if is_server {
817            badge.server_cert_fingerprint()
818        } else {
819            badge.identity_cert_fingerprint()
820        };
821
822        tracing::debug!(
823            expected = %expected_fp,
824            actual = %cert.fingerprint,
825            "Comparing certificate fingerprints"
826        );
827
828        if !cert.fingerprint.matches(expected_fp) {
829            tracing::error!(
830                expected = %expected_fp,
831                actual = %cert.fingerprint,
832                "Certificate fingerprint MISMATCH"
833            );
834            return VerificationOutcome::FingerprintMismatch {
835                expected: expected_fp.to_string(),
836                actual: cert.fingerprint.to_string(),
837                badge: badge.clone(),
838            };
839        }
840        tracing::debug!("Fingerprint matches");
841
842        // Compare hostname
843        let expected_host = badge.agent_host();
844        let actual_host = cert.fqdn().unwrap_or("");
845
846        tracing::debug!(
847            expected = %expected_host,
848            actual = %actual_host,
849            "Comparing hostnames"
850        );
851
852        if !actual_host.eq_ignore_ascii_case(expected_host) {
853            tracing::error!(
854                expected = %expected_host,
855                actual = %actual_host,
856                "Hostname MISMATCH"
857            );
858            return VerificationOutcome::HostnameMismatch {
859                expected: expected_host.to_string(),
860                actual: actual_host.to_string(),
861                badge: badge.clone(),
862            };
863        }
864
865        tracing::info!(
866            agent = %badge.agent_name(),
867            host = %badge.agent_host(),
868            "Verification SUCCESSFUL"
869        );
870        VerificationOutcome::Verified {
871            badge: badge.clone(),
872            matched_fingerprint: cert.fingerprint.clone(),
873        }
874    }
875
876    async fn handle_dns_error(
877        &self,
878        error: DnsError,
879        fqdn: &Fqdn,
880        cert: &CertIdentity,
881    ) -> VerificationOutcome {
882        match self.failure_policy {
883            FailurePolicy::FailClosed => VerificationOutcome::DnsError(error),
884            FailurePolicy::FailOpenWithCache { max_staleness } => {
885                if let Some(cache) = &self.cache {
886                    for cached in cache.get_all_for_fqdn(fqdn).await {
887                        if cached.fetched_at.elapsed() < max_staleness {
888                            let outcome = self.verify_against_badge(&cached.badge, cert, true);
889                            if outcome.is_success() {
890                                return outcome;
891                            }
892                        }
893                    }
894                }
895                VerificationOutcome::DnsError(error)
896            }
897        }
898    }
899
900    async fn handle_ans_error(
901        &self,
902        error: AnsError,
903        fqdn: &Fqdn,
904        cert: &CertIdentity,
905    ) -> VerificationOutcome {
906        match self.failure_policy {
907            FailurePolicy::FailClosed => match error {
908                AnsError::TransparencyLog(e) => VerificationOutcome::TlogError(e),
909                AnsError::Dns(e) => VerificationOutcome::DnsError(e),
910                AnsError::Certificate(e) => VerificationOutcome::CertError(e),
911                AnsError::Parse(e) => VerificationOutcome::ParseError(e),
912                AnsError::Verification(_) => VerificationOutcome::NotAnsAgent {
913                    fqdn: fqdn.to_string(),
914                },
915            },
916            FailurePolicy::FailOpenWithCache { max_staleness } => {
917                if let Some(cache) = &self.cache {
918                    for cached in cache.get_all_for_fqdn(fqdn).await {
919                        if cached.fetched_at.elapsed() < max_staleness {
920                            let outcome = self.verify_against_badge(&cached.badge, cert, true);
921                            if outcome.is_success() {
922                                return outcome;
923                            }
924                        }
925                    }
926                }
927                match error {
928                    AnsError::TransparencyLog(e) => VerificationOutcome::TlogError(e),
929                    AnsError::Dns(e) => VerificationOutcome::DnsError(e),
930                    AnsError::Certificate(e) => VerificationOutcome::CertError(e),
931                    AnsError::Parse(e) => VerificationOutcome::ParseError(e),
932                    AnsError::Verification(_) => VerificationOutcome::NotAnsAgent {
933                        fqdn: fqdn.to_string(),
934                    },
935                }
936            }
937        }
938    }
939}
940
941/// Builder for `ServerVerifier`.
942#[derive(Default)]
943pub struct ServerVerifierBuilder {
944    dns_resolver: Option<Arc<dyn DnsResolver>>,
945    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
946    cache: Option<Arc<BadgeCache>>,
947    failure_policy: FailurePolicy,
948    dane_policy: DanePolicy,
949    dane_port: Option<u16>,
950    trusted_ra_domains: Option<HashSet<String>>,
951}
952
953impl fmt::Debug for ServerVerifierBuilder {
954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
955        f.debug_struct("ServerVerifierBuilder")
956            .field("failure_policy", &self.failure_policy)
957            .field("dane_policy", &self.dane_policy)
958            .field("dane_port", &self.dane_port)
959            .field("has_dns_resolver", &self.dns_resolver.is_some())
960            .field("has_tlog_client", &self.tlog_client.is_some())
961            .field("has_cache", &self.cache.is_some())
962            .finish_non_exhaustive()
963    }
964}
965
966impl ServerVerifierBuilder {
967    /// Set a custom DNS resolver.
968    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
969        self.dns_resolver = Some(resolver);
970        self
971    }
972
973    /// Set a custom transparency log client.
974    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
975        self.tlog_client = Some(client);
976        self
977    }
978
979    /// Enable caching with default configuration.
980    pub fn with_cache(mut self) -> Self {
981        self.cache = Some(Arc::new(BadgeCache::with_defaults()));
982        self
983    }
984
985    /// Enable caching with custom configuration.
986    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
987        self.cache = Some(Arc::new(BadgeCache::new(config)));
988        self
989    }
990
991    /// Use an existing cache.
992    pub fn cache(mut self, cache: Arc<BadgeCache>) -> Self {
993        self.cache = Some(cache);
994        self
995    }
996
997    /// Set the failure policy.
998    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
999        self.failure_policy = policy;
1000        self
1001    }
1002
1003    /// Set the DANE/TLSA verification policy.
1004    ///
1005    /// - `DanePolicy::Disabled`: Skip DANE verification entirely (default)
1006    /// - `DanePolicy::ValidateIfPresent`: Verify TLSA if records exist, skip if not
1007    /// - `DanePolicy::Required`: Require TLSA records to exist and match
1008    pub fn dane_policy(mut self, policy: DanePolicy) -> Self {
1009        self.dane_policy = policy;
1010        self
1011    }
1012
1013    /// Enable DANE verification if TLSA records are present.
1014    ///
1015    /// This is a convenience method equivalent to `.dane_policy(DanePolicy::ValidateIfPresent)`.
1016    pub fn with_dane_if_present(mut self) -> Self {
1017        self.dane_policy = DanePolicy::ValidateIfPresent;
1018        self
1019    }
1020
1021    /// Require DANE verification (fail if no TLSA records).
1022    ///
1023    /// This is a convenience method equivalent to `.dane_policy(DanePolicy::Required)`.
1024    pub fn require_dane(mut self) -> Self {
1025        self.dane_policy = DanePolicy::Required;
1026        self
1027    }
1028
1029    /// Set the port for TLSA lookups (default: 443).
1030    pub fn dane_port(mut self, port: u16) -> Self {
1031        self.dane_port = Some(port);
1032        self
1033    }
1034
1035    /// Restrict badge URL fetches to a set of trusted RA domains.
1036    ///
1037    /// When configured, badge URLs discovered via DNS TXT records will be
1038    /// validated against this set before any HTTP request is made. URLs
1039    /// pointing to hosts not in the set are rejected with
1040    /// `TlogError::UntrustedDomain`.
1041    ///
1042    /// By default (`None`), all domains are allowed.
1043    pub fn trusted_ra_domains(
1044        mut self,
1045        domains: impl IntoIterator<Item = impl Into<String>>,
1046    ) -> Self {
1047        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
1048        self
1049    }
1050
1051    /// Build the verifier.
1052    pub async fn build(self) -> AnsResult<ServerVerifier> {
1053        let dns_resolver = match self.dns_resolver {
1054            Some(r) => r,
1055            None => Arc::new(
1056                HickoryDnsResolver::new()
1057                    .await
1058                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1059            ),
1060        };
1061
1062        let tlog_client = self
1063            .tlog_client
1064            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
1065
1066        Ok(ServerVerifier {
1067            dns_resolver,
1068            tlog_client,
1069            cache: self.cache,
1070            failure_policy: self.failure_policy,
1071            dane_policy: self.dane_policy,
1072            dane_port: self.dane_port.unwrap_or(443),
1073            trusted_ra_domains: self.trusted_ra_domains,
1074        })
1075    }
1076}
1077
1078/// Client verifier for servers verifying mTLS agent clients.
1079pub struct ClientVerifier {
1080    dns_resolver: Arc<dyn DnsResolver>,
1081    tlog_client: Arc<dyn TransparencyLogClient>,
1082    cache: Option<Arc<BadgeCache>>,
1083    failure_policy: FailurePolicy,
1084    /// Optional set of trusted RA domains for badge URL validation.
1085    trusted_ra_domains: Option<HashSet<String>>,
1086}
1087
1088impl fmt::Debug for ClientVerifier {
1089    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1090        f.debug_struct("ClientVerifier")
1091            .field("failure_policy", &self.failure_policy)
1092            .field("has_cache", &self.cache.is_some())
1093            .field("has_trusted_ra_domains", &self.trusted_ra_domains.is_some())
1094            .finish_non_exhaustive()
1095    }
1096}
1097
1098impl ClientVerifier {
1099    /// Create a new builder.
1100    pub fn builder() -> ClientVerifierBuilder {
1101        ClientVerifierBuilder::default()
1102    }
1103
1104    /// Verify an mTLS client certificate.
1105    ///
1106    /// # Steps
1107    /// 1. Extract CN (FQDN) and URI SAN (`ANSName`) from certificate
1108    /// 2. Parse version from `ANSName`
1109    /// 3. DNS lookup for `_ans-badge` (with `_ra-badge` fallback) using CN as FQDN
1110    /// 4. Select badge matching version from certificate
1111    /// 5. Compare identity cert fingerprint, CN, and URI SAN to badge
1112    /// 6. On fingerprint mismatch, refresh badge and re-verify once
1113    #[allow(clippy::too_many_lines)] // verification flow reads best as a single method
1114    pub async fn verify(&self, client_cert: &CertIdentity) -> VerificationOutcome {
1115        tracing::info!("Starting mTLS client verification");
1116        tracing::debug!(
1117            cn = ?client_cert.common_name,
1118            dns_sans = ?client_cert.dns_sans,
1119            uri_sans = ?client_cert.uri_sans,
1120            fingerprint = %client_cert.fingerprint,
1121            "Client certificate details"
1122        );
1123
1124        // Extract FQDN from certificate
1125        let Some(fqdn_str) = client_cert.fqdn() else {
1126            tracing::error!("No CN or DNS SAN found in client certificate");
1127            return VerificationOutcome::CertError(CryptoError::NoCommonName);
1128        };
1129
1130        let fqdn = match Fqdn::new(fqdn_str) {
1131            Ok(f) => f,
1132            Err(e) => {
1133                tracing::error!(fqdn = %fqdn_str, error = %e, "Invalid FQDN in certificate");
1134                return VerificationOutcome::ParseError(e);
1135            }
1136        };
1137        tracing::debug!(fqdn = %fqdn, "Extracted FQDN from certificate");
1138
1139        // Extract ANS name and version from URI SAN
1140        let ans_name = if let Some(n) = client_cert.ans_name() {
1141            tracing::debug!(ans_name = %n, "Found ANS name in URI SAN");
1142            n
1143        } else {
1144            tracing::error!(uri_sans = ?client_cert.uri_sans, "No ANS name (ans://) found in URI SANs");
1145            return VerificationOutcome::CertError(CryptoError::NoUriSan);
1146        };
1147
1148        let version = ans_name.version().clone();
1149        tracing::debug!(version = %version, "Parsed version from ANS name");
1150
1151        // Check cache first
1152        if let Some(cache) = &self.cache
1153            && let Some(cached) = cache.get_by_fqdn_version(&fqdn, &version).await
1154        {
1155            tracing::debug!(fqdn = %fqdn, version = %version, "Using cached badge");
1156            let outcome = self.verify_client_against_badge(&cached.badge, client_cert, &ans_name);
1157            // Refresh-on-mismatch for cached badges
1158            if matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }) {
1159                tracing::info!(fqdn = %fqdn, "Fingerprint mismatch on cached badge, refreshing");
1160                return self
1161                    .verify_client_with_refresh(&fqdn, &version, client_cert, &ans_name)
1162                    .await;
1163            }
1164            return outcome;
1165        }
1166
1167        // DNS lookup
1168        tracing::debug!(fqdn = %fqdn, version = %version, "Looking up badge for version");
1169        let badge_record = match self
1170            .dns_resolver
1171            .find_badge_for_version(&fqdn, &version)
1172            .await
1173        {
1174            Ok(Some(record)) => {
1175                tracing::debug!(url = %record.url, "Found badge record for version");
1176                record
1177            }
1178            Ok(None) => {
1179                tracing::debug!("No badge for specific version, trying preferred badge");
1180                // Try to find any badge
1181                match self.dns_resolver.find_preferred_badge(&fqdn).await {
1182                    Ok(Some(record)) => {
1183                        tracing::debug!(url = %record.url, version = ?record.version, "Using preferred badge");
1184                        record
1185                    }
1186                    Ok(None) => {
1187                        tracing::warn!(fqdn = %fqdn, "No badge record found - not an ANS agent");
1188                        return VerificationOutcome::NotAnsAgent {
1189                            fqdn: fqdn.to_string(),
1190                        };
1191                    }
1192                    Err(e) => {
1193                        tracing::error!(error = %e, "DNS lookup failed");
1194                        return self
1195                            .handle_dns_error(e, &fqdn, &version, client_cert, &ans_name)
1196                            .await;
1197                    }
1198                }
1199            }
1200            Err(e) => {
1201                tracing::error!(error = %e, "DNS lookup failed");
1202                return self
1203                    .handle_dns_error(e, &fqdn, &version, client_cert, &ans_name)
1204                    .await;
1205            }
1206        };
1207
1208        // Validate badge URL domain before fetching
1209        if let Err(e) = validate_badge_domain(self.trusted_ra_domains.as_ref(), &badge_record.url) {
1210            return self
1211                .handle_tlog_error(e, &fqdn, &version, client_cert, &ans_name)
1212                .await;
1213        }
1214
1215        // Fetch badge
1216        tracing::debug!(url = %badge_record.url, "Fetching badge from transparency log");
1217        let badge = match self.tlog_client.fetch_badge(&badge_record.url).await {
1218            Ok(b) => {
1219                tracing::debug!(
1220                    status = ?b.status,
1221                    agent_host = %b.agent_host(),
1222                    ans_name = %b.agent_name(),
1223                    "Fetched badge successfully"
1224                );
1225                b
1226            }
1227            Err(e) => {
1228                tracing::error!(url = %badge_record.url, error = %e, "Failed to fetch badge");
1229                return self
1230                    .handle_tlog_error(e, &fqdn, &version, client_cert, &ans_name)
1231                    .await;
1232            }
1233        };
1234
1235        // Cache the badge (tracked for multi-version lookups)
1236        if let Some(cache) = &self.cache {
1237            cache
1238                .insert_for_fqdn_version(&fqdn, &version, badge.clone())
1239                .await;
1240            tracing::debug!(fqdn = %fqdn, version = %version, "Cached badge");
1241        }
1242
1243        let outcome = self.verify_client_against_badge(&badge, client_cert, &ans_name);
1244        // Refresh-on-mismatch for freshly fetched badges
1245        if matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }) {
1246            tracing::info!(fqdn = %fqdn, "Fingerprint mismatch, attempting refresh");
1247            return self
1248                .verify_client_with_refresh(&fqdn, &version, client_cert, &ans_name)
1249                .await;
1250        }
1251        outcome
1252    }
1253
1254    #[allow(clippy::unused_self)] // logically part of ClientVerifier; may use self in future
1255    fn verify_client_against_badge(
1256        &self,
1257        badge: &Badge,
1258        cert: &CertIdentity,
1259        ans_name: &AnsName,
1260    ) -> VerificationOutcome {
1261        tracing::debug!("Verifying client certificate against badge");
1262
1263        // Check status
1264        if badge.status.should_reject() {
1265            tracing::warn!(status = ?badge.status, "Badge status is not valid for connections");
1266            return VerificationOutcome::InvalidStatus {
1267                status: badge.status,
1268                badge: badge.clone(),
1269            };
1270        }
1271        tracing::debug!(status = ?badge.status, "Badge status is valid");
1272
1273        // Compare fingerprint (identity cert for mTLS clients)
1274        let expected_fp = badge.identity_cert_fingerprint();
1275        tracing::debug!(
1276            expected = %expected_fp,
1277            actual = %cert.fingerprint,
1278            "Comparing identity certificate fingerprints"
1279        );
1280
1281        if !cert.fingerprint.matches(expected_fp) {
1282            tracing::error!(
1283                expected = %expected_fp,
1284                actual = %cert.fingerprint,
1285                "Identity certificate fingerprint MISMATCH"
1286            );
1287            return VerificationOutcome::FingerprintMismatch {
1288                expected: expected_fp.to_string(),
1289                actual: cert.fingerprint.to_string(),
1290                badge: badge.clone(),
1291            };
1292        }
1293        tracing::debug!("Identity fingerprint matches");
1294
1295        // Compare hostname
1296        let expected_host = badge.agent_host();
1297        let actual_host = cert.fqdn().unwrap_or("");
1298        tracing::debug!(
1299            expected = %expected_host,
1300            actual = %actual_host,
1301            "Comparing hostnames"
1302        );
1303
1304        if !actual_host.eq_ignore_ascii_case(expected_host) {
1305            tracing::error!(
1306                expected = %expected_host,
1307                actual = %actual_host,
1308                "Hostname MISMATCH"
1309            );
1310            return VerificationOutcome::HostnameMismatch {
1311                expected: expected_host.to_string(),
1312                actual: actual_host.to_string(),
1313                badge: badge.clone(),
1314            };
1315        }
1316        tracing::debug!("Hostname matches");
1317
1318        // Compare ANS name
1319        let expected_ans_name = badge.agent_name();
1320        tracing::debug!(
1321            expected = %expected_ans_name,
1322            actual = %ans_name,
1323            "Comparing ANS names"
1324        );
1325
1326        if ans_name.to_string() != expected_ans_name {
1327            tracing::error!(
1328                expected = %expected_ans_name,
1329                actual = %ans_name,
1330                "ANS name MISMATCH"
1331            );
1332            return VerificationOutcome::AnsNameMismatch {
1333                expected: expected_ans_name.to_string(),
1334                actual: ans_name.to_string(),
1335                badge: badge.clone(),
1336            };
1337        }
1338
1339        tracing::info!(
1340            agent = %badge.agent_name(),
1341            host = %badge.agent_host(),
1342            "Client verification SUCCESSFUL"
1343        );
1344        VerificationOutcome::Verified {
1345            badge: badge.clone(),
1346            matched_fingerprint: cert.fingerprint.clone(),
1347        }
1348    }
1349
1350    /// Refresh-on-mismatch for client verification.
1351    ///
1352    /// Invalidates the cache, re-fetches the badge from the transparency log,
1353    /// and re-verifies. This handles certificate renewals where the badge
1354    /// was updated but the verifier had a stale copy.
1355    async fn verify_client_with_refresh(
1356        &self,
1357        fqdn: &Fqdn,
1358        version: &Version,
1359        client_cert: &CertIdentity,
1360        ans_name: &AnsName,
1361    ) -> VerificationOutcome {
1362        // Invalidate cache
1363        if let Some(cache) = &self.cache {
1364            cache
1365                .invalidate(&CacheKey::fqdn_version(fqdn, version))
1366                .await;
1367        }
1368
1369        // Re-fetch from DNS
1370        let badge_record = match self
1371            .dns_resolver
1372            .find_badge_for_version(fqdn, version)
1373            .await
1374        {
1375            Ok(Some(record)) => record,
1376            Ok(None) => match self.dns_resolver.find_preferred_badge(fqdn).await {
1377                Ok(Some(record)) => record,
1378                Ok(None) => {
1379                    return VerificationOutcome::NotAnsAgent {
1380                        fqdn: fqdn.to_string(),
1381                    };
1382                }
1383                Err(e) => return VerificationOutcome::DnsError(e),
1384            },
1385            Err(e) => return VerificationOutcome::DnsError(e),
1386        };
1387
1388        // Validate badge URL domain before re-fetch
1389        if let Err(e) = validate_badge_domain(self.trusted_ra_domains.as_ref(), &badge_record.url) {
1390            return VerificationOutcome::TlogError(e);
1391        }
1392
1393        // Re-fetch badge from transparency log
1394        let badge = match self.tlog_client.fetch_badge(&badge_record.url).await {
1395            Ok(b) => b,
1396            Err(e) => return VerificationOutcome::TlogError(e),
1397        };
1398
1399        // Cache the refreshed badge (tracked for multi-version lookups)
1400        if let Some(cache) = &self.cache {
1401            cache
1402                .insert_for_fqdn_version(fqdn, version, badge.clone())
1403                .await;
1404        }
1405
1406        // Re-verify — this is the final answer
1407        self.verify_client_against_badge(&badge, client_cert, ans_name)
1408    }
1409
1410    async fn handle_dns_error(
1411        &self,
1412        error: DnsError,
1413        fqdn: &Fqdn,
1414        version: &Version,
1415        cert: &CertIdentity,
1416        ans_name: &AnsName,
1417    ) -> VerificationOutcome {
1418        match self.failure_policy {
1419            FailurePolicy::FailClosed => VerificationOutcome::DnsError(error),
1420            FailurePolicy::FailOpenWithCache { max_staleness } => {
1421                if let Some(cache) = &self.cache
1422                    && let Some(cached) = cache.get_by_fqdn_version(fqdn, version).await
1423                    && cached.fetched_at.elapsed() < max_staleness
1424                {
1425                    return self.verify_client_against_badge(&cached.badge, cert, ans_name);
1426                }
1427                VerificationOutcome::DnsError(error)
1428            }
1429        }
1430    }
1431
1432    async fn handle_tlog_error(
1433        &self,
1434        error: TlogError,
1435        fqdn: &Fqdn,
1436        version: &Version,
1437        cert: &CertIdentity,
1438        ans_name: &AnsName,
1439    ) -> VerificationOutcome {
1440        match self.failure_policy {
1441            FailurePolicy::FailClosed => VerificationOutcome::TlogError(error),
1442            FailurePolicy::FailOpenWithCache { max_staleness } => {
1443                if let Some(cache) = &self.cache
1444                    && let Some(cached) = cache.get_by_fqdn_version(fqdn, version).await
1445                    && cached.fetched_at.elapsed() < max_staleness
1446                {
1447                    return self.verify_client_against_badge(&cached.badge, cert, ans_name);
1448                }
1449                VerificationOutcome::TlogError(error)
1450            }
1451        }
1452    }
1453}
1454
1455/// Builder for `ClientVerifier`.
1456#[derive(Default)]
1457pub struct ClientVerifierBuilder {
1458    dns_resolver: Option<Arc<dyn DnsResolver>>,
1459    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
1460    cache: Option<Arc<BadgeCache>>,
1461    failure_policy: FailurePolicy,
1462    trusted_ra_domains: Option<HashSet<String>>,
1463}
1464
1465impl fmt::Debug for ClientVerifierBuilder {
1466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1467        f.debug_struct("ClientVerifierBuilder")
1468            .field("failure_policy", &self.failure_policy)
1469            .field("has_dns_resolver", &self.dns_resolver.is_some())
1470            .field("has_tlog_client", &self.tlog_client.is_some())
1471            .field("has_cache", &self.cache.is_some())
1472            .finish_non_exhaustive()
1473    }
1474}
1475
1476impl ClientVerifierBuilder {
1477    /// Set a custom DNS resolver.
1478    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
1479        self.dns_resolver = Some(resolver);
1480        self
1481    }
1482
1483    /// Set a custom transparency log client.
1484    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
1485        self.tlog_client = Some(client);
1486        self
1487    }
1488
1489    /// Enable caching with default configuration.
1490    pub fn with_cache(mut self) -> Self {
1491        self.cache = Some(Arc::new(BadgeCache::with_defaults()));
1492        self
1493    }
1494
1495    /// Enable caching with custom configuration.
1496    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
1497        self.cache = Some(Arc::new(BadgeCache::new(config)));
1498        self
1499    }
1500
1501    /// Use an existing cache.
1502    pub fn cache(mut self, cache: Arc<BadgeCache>) -> Self {
1503        self.cache = Some(cache);
1504        self
1505    }
1506
1507    /// Set the failure policy.
1508    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
1509        self.failure_policy = policy;
1510        self
1511    }
1512
1513    /// Restrict badge URL fetches to a set of trusted RA domains.
1514    ///
1515    /// When configured, badge URLs discovered via DNS TXT records will be
1516    /// validated against this set before any HTTP request is made. URLs
1517    /// pointing to hosts not in the set are rejected with
1518    /// `TlogError::UntrustedDomain`.
1519    ///
1520    /// By default (`None`), all domains are allowed.
1521    pub fn trusted_ra_domains(
1522        mut self,
1523        domains: impl IntoIterator<Item = impl Into<String>>,
1524    ) -> Self {
1525        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
1526        self
1527    }
1528
1529    /// Build the verifier.
1530    pub async fn build(self) -> AnsResult<ClientVerifier> {
1531        let dns_resolver = match self.dns_resolver {
1532            Some(r) => r,
1533            None => Arc::new(
1534                HickoryDnsResolver::new()
1535                    .await
1536                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1537            ),
1538        };
1539
1540        let tlog_client = self
1541            .tlog_client
1542            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
1543
1544        Ok(ClientVerifier {
1545            dns_resolver,
1546            tlog_client,
1547            cache: self.cache,
1548            failure_policy: self.failure_policy,
1549            trusted_ra_domains: self.trusted_ra_domains,
1550        })
1551    }
1552}
1553
1554/// High-level ANS verifier combining server and client verification.
1555pub struct AnsVerifier {
1556    server_verifier: ServerVerifier,
1557    client_verifier: ClientVerifier,
1558    #[cfg(feature = "rustls")]
1559    private_ca_pem: Option<Vec<u8>>,
1560}
1561
1562impl fmt::Debug for AnsVerifier {
1563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1564        f.debug_struct("AnsVerifier")
1565            .field("server_verifier", &self.server_verifier)
1566            .field("client_verifier", &self.client_verifier)
1567            .finish_non_exhaustive()
1568    }
1569}
1570
1571impl AnsVerifier {
1572    /// Create a new verifier with default configuration.
1573    pub async fn new() -> AnsResult<Self> {
1574        Self::builder().build().await
1575    }
1576
1577    /// Create a builder for custom configuration.
1578    pub fn builder() -> AnsVerifierBuilder {
1579        AnsVerifierBuilder::default()
1580    }
1581
1582    /// Verify an agent server (client-side verification).
1583    pub async fn verify_server(
1584        &self,
1585        fqdn: impl AsRef<str>,
1586        server_cert: &CertIdentity,
1587    ) -> VerificationOutcome {
1588        let fqdn = match Fqdn::new(fqdn.as_ref()) {
1589            Ok(f) => f,
1590            Err(e) => return VerificationOutcome::ParseError(e),
1591        };
1592        self.server_verifier.verify(&fqdn, server_cert).await
1593    }
1594
1595    /// Verify an mTLS client (server-side verification).
1596    pub async fn verify_client(&self, client_cert: &CertIdentity) -> VerificationOutcome {
1597        self.client_verifier.verify(client_cert).await
1598    }
1599
1600    /// Pre-fetch a badge for caching (before TLS connection).
1601    pub async fn prefetch(&self, fqdn: impl AsRef<str>) -> AnsResult<Badge> {
1602        let fqdn = Fqdn::new(fqdn.as_ref())?;
1603        self.server_verifier.prefetch(&fqdn).await
1604    }
1605
1606    /// Create a rustls [`AnsClientCertVerifier`](crate::AnsClientCertVerifier) for server-side mTLS.
1607    ///
1608    /// Requires `private_ca_pem` to have been set on the builder.
1609    /// Returns a verifier that validates client certificate chains against the ANS Private CA.
1610    /// Client certificates are **required**.
1611    #[cfg(feature = "rustls")]
1612    pub fn client_cert_verifier(&self) -> AnsResult<crate::AnsClientCertVerifier> {
1613        let pem = self.private_ca_pem.as_ref().ok_or_else(|| {
1614            AnsError::Verification(VerificationError::Configuration(
1615                "private_ca_pem is required for client_cert_verifier".into(),
1616            ))
1617        })?;
1618        crate::AnsClientCertVerifier::from_pem(pem).map_err(|e| {
1619            AnsError::Verification(VerificationError::Configuration(format!(
1620                "Failed to build client cert verifier: {e}"
1621            )))
1622        })
1623    }
1624
1625    /// Create a rustls [`AnsClientCertVerifier`](crate::AnsClientCertVerifier) that allows optional client certs.
1626    ///
1627    /// Requires `private_ca_pem` to have been set on the builder.
1628    /// Use this when the server should accept both authenticated and unauthenticated clients.
1629    #[cfg(feature = "rustls")]
1630    pub fn client_cert_verifier_optional(&self) -> AnsResult<crate::AnsClientCertVerifier> {
1631        let pem = self.private_ca_pem.as_ref().ok_or_else(|| {
1632            AnsError::Verification(VerificationError::Configuration(
1633                "private_ca_pem is required for client_cert_verifier_optional".into(),
1634            ))
1635        })?;
1636        crate::AnsClientCertVerifier::from_pem_optional(pem).map_err(|e| {
1637            AnsError::Verification(VerificationError::Configuration(format!(
1638                "Failed to build optional client cert verifier: {e}"
1639            )))
1640        })
1641    }
1642
1643    /// Create a rustls [`AnsServerCertVerifier`](crate::AnsServerCertVerifier) for a specific badge fingerprint.
1644    ///
1645    /// Typically called after [`prefetch()`](Self::prefetch) to get the expected fingerprint
1646    /// from the badge's `attestations.server_cert.fingerprint` field.
1647    #[cfg(feature = "rustls")]
1648    pub fn server_cert_verifier(
1649        &self,
1650        fingerprint: &CertFingerprint,
1651    ) -> AnsResult<crate::AnsServerCertVerifier> {
1652        crate::AnsServerCertVerifier::new(fingerprint.clone()).map_err(|e| {
1653            AnsError::Verification(VerificationError::Configuration(format!(
1654                "Failed to build server cert verifier: {e}"
1655            )))
1656        })
1657    }
1658}
1659
1660/// Builder for `AnsVerifier`.
1661#[derive(Default)]
1662pub struct AnsVerifierBuilder {
1663    dns_resolver: Option<Arc<dyn DnsResolver>>,
1664    dns_config: Option<DnsResolverConfig>,
1665    dns_nameservers: Option<Vec<std::net::Ipv4Addr>>,
1666    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
1667    cache_config: Option<CacheConfig>,
1668    failure_policy: FailurePolicy,
1669    dane_policy: DanePolicy,
1670    dane_port: Option<u16>,
1671    trusted_ra_domains: Option<HashSet<String>>,
1672    #[cfg(feature = "rustls")]
1673    private_ca_pem: Option<Vec<u8>>,
1674}
1675
1676impl fmt::Debug for AnsVerifierBuilder {
1677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1678        f.debug_struct("AnsVerifierBuilder")
1679            .field("dns_config", &self.dns_config)
1680            .field("failure_policy", &self.failure_policy)
1681            .field("dane_policy", &self.dane_policy)
1682            .field("dane_port", &self.dane_port)
1683            .field("has_dns_resolver", &self.dns_resolver.is_some())
1684            .field("has_tlog_client", &self.tlog_client.is_some())
1685            .field("has_cache_config", &self.cache_config.is_some())
1686            .finish_non_exhaustive()
1687    }
1688}
1689
1690impl AnsVerifierBuilder {
1691    /// Set a custom DNS resolver.
1692    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
1693        self.dns_resolver = Some(resolver);
1694        self
1695    }
1696
1697    /// Use a preset DNS resolver configuration (Cloudflare, Google, etc.).
1698    ///
1699    /// # Example
1700    /// ```rust,no_run
1701    /// use ans_verify::{AnsVerifier, DnsResolverConfig};
1702    ///
1703    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1704    /// let verifier = AnsVerifier::builder()
1705    ///     .dns_preset(DnsResolverConfig::CloudflareTls)
1706    ///     .build()
1707    ///     .await?;
1708    /// # Ok(())
1709    /// # }
1710    /// ```
1711    pub fn dns_preset(mut self, preset: DnsResolverConfig) -> Self {
1712        self.dns_config = Some(preset);
1713        self
1714    }
1715
1716    /// Use Cloudflare DNS (1.1.1.1).
1717    pub fn dns_cloudflare(self) -> Self {
1718        self.dns_preset(DnsResolverConfig::Cloudflare)
1719    }
1720
1721    /// Use Cloudflare DNS over TLS.
1722    pub fn dns_cloudflare_tls(self) -> Self {
1723        self.dns_preset(DnsResolverConfig::CloudflareTls)
1724    }
1725
1726    /// Use Google Public DNS (8.8.8.8).
1727    pub fn dns_google(self) -> Self {
1728        self.dns_preset(DnsResolverConfig::Google)
1729    }
1730
1731    /// Use Google DNS over TLS.
1732    pub fn dns_google_tls(self) -> Self {
1733        self.dns_preset(DnsResolverConfig::GoogleTls)
1734    }
1735
1736    /// Use Quad9 DNS (9.9.9.9).
1737    pub fn dns_quad9(self) -> Self {
1738        self.dns_preset(DnsResolverConfig::Quad9)
1739    }
1740
1741    /// Use custom DNS nameservers.
1742    ///
1743    /// # Example
1744    /// ```rust,no_run
1745    /// use ans_verify::AnsVerifier;
1746    /// use std::net::Ipv4Addr;
1747    ///
1748    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1749    /// let verifier = AnsVerifier::builder()
1750    ///     .dns_nameservers(&[
1751    ///         Ipv4Addr::new(1, 1, 1, 1),
1752    ///         Ipv4Addr::new(8, 8, 8, 8),
1753    ///     ])
1754    ///     .build()
1755    ///     .await?;
1756    /// # Ok(())
1757    /// # }
1758    /// ```
1759    pub fn dns_nameservers(mut self, nameservers: &[std::net::Ipv4Addr]) -> Self {
1760        self.dns_nameservers = Some(nameservers.to_vec());
1761        self
1762    }
1763
1764    /// Set a custom transparency log client.
1765    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
1766        self.tlog_client = Some(client);
1767        self
1768    }
1769
1770    /// Enable caching with default configuration.
1771    pub fn with_caching(mut self) -> Self {
1772        self.cache_config = Some(CacheConfig::default());
1773        self
1774    }
1775
1776    /// Enable caching with custom configuration.
1777    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
1778        self.cache_config = Some(config);
1779        self
1780    }
1781
1782    /// Set the failure policy.
1783    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
1784        self.failure_policy = policy;
1785        self
1786    }
1787
1788    /// Set the DANE/TLSA verification policy.
1789    ///
1790    /// - `DanePolicy::Disabled`: Skip DANE verification entirely (default)
1791    /// - `DanePolicy::ValidateIfPresent`: Verify TLSA if records exist, skip if not
1792    /// - `DanePolicy::Required`: Require TLSA records to exist and match
1793    pub fn dane_policy(mut self, policy: DanePolicy) -> Self {
1794        self.dane_policy = policy;
1795        self
1796    }
1797
1798    /// Enable DANE verification if TLSA records are present.
1799    pub fn with_dane_if_present(mut self) -> Self {
1800        self.dane_policy = DanePolicy::ValidateIfPresent;
1801        self
1802    }
1803
1804    /// Require DANE verification (fail if no TLSA records).
1805    pub fn require_dane(mut self) -> Self {
1806        self.dane_policy = DanePolicy::Required;
1807        self
1808    }
1809
1810    /// Set the port for TLSA lookups (default: 443).
1811    pub fn dane_port(mut self, port: u16) -> Self {
1812        self.dane_port = Some(port);
1813        self
1814    }
1815
1816    /// Restrict badge URL fetches to a set of trusted RA domains.
1817    ///
1818    /// When configured, badge URLs discovered via DNS TXT records will be
1819    /// validated against this set before any HTTP request is made.
1820    pub fn trusted_ra_domains(
1821        mut self,
1822        domains: impl IntoIterator<Item = impl Into<String>>,
1823    ) -> Self {
1824        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
1825        self
1826    }
1827
1828    /// Set the ANS Private CA certificate (PEM-encoded).
1829    ///
1830    /// Required for mTLS client verification. The Private CA is used during
1831    /// the TLS handshake to validate that client certificates chain to the
1832    /// ANS Private CA. Different environments (OTE, PROD) use different CAs.
1833    ///
1834    /// The PEM bytes are typically loaded from configuration, not hardcoded.
1835    #[cfg(feature = "rustls")]
1836    pub fn private_ca_pem(mut self, pem: impl Into<Vec<u8>>) -> Self {
1837        self.private_ca_pem = Some(pem.into());
1838        self
1839    }
1840
1841    /// Build the verifier.
1842    pub async fn build(self) -> AnsResult<AnsVerifier> {
1843        // Determine DNS resolver: custom > nameservers > preset > default
1844        let dns_resolver: Arc<dyn DnsResolver> = if let Some(r) = self.dns_resolver {
1845            r
1846        } else if let Some(nameservers) = self.dns_nameservers {
1847            Arc::new(
1848                HickoryDnsResolver::with_nameservers(&nameservers)
1849                    .await
1850                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1851            )
1852        } else if let Some(preset) = self.dns_config {
1853            Arc::new(
1854                HickoryDnsResolver::with_preset(preset)
1855                    .await
1856                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1857            )
1858        } else {
1859            Arc::new(
1860                HickoryDnsResolver::new()
1861                    .await
1862                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1863            )
1864        };
1865
1866        let tlog_client: Arc<dyn TransparencyLogClient> = self
1867            .tlog_client
1868            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
1869
1870        let cache = self.cache_config.map(|c| Arc::new(BadgeCache::new(c)));
1871        let dane_port = self.dane_port.unwrap_or(443);
1872
1873        let server_verifier = ServerVerifier {
1874            dns_resolver: dns_resolver.clone(),
1875            tlog_client: tlog_client.clone(),
1876            cache: cache.clone(),
1877            failure_policy: self.failure_policy,
1878            dane_policy: self.dane_policy,
1879            dane_port,
1880            trusted_ra_domains: self.trusted_ra_domains.clone(),
1881        };
1882
1883        let client_verifier = ClientVerifier {
1884            dns_resolver,
1885            tlog_client,
1886            cache,
1887            failure_policy: self.failure_policy,
1888            trusted_ra_domains: self.trusted_ra_domains,
1889        };
1890
1891        Ok(AnsVerifier {
1892            server_verifier,
1893            client_verifier,
1894            #[cfg(feature = "rustls")]
1895            private_ca_pem: self.private_ca_pem,
1896        })
1897    }
1898}
1899
1900#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1901#[cfg(test)]
1902mod tests {
1903    use super::*;
1904    use crate::dns::MockDnsResolver;
1905    use crate::tlog::MockTransparencyLogClient;
1906    use chrono::Utc;
1907    use uuid::Uuid;
1908
1909    // Compile-time thread safety proof — fails compilation if any field
1910    // breaks Send/Sync (e.g., Rc, RefCell added to a struct).
1911    const fn _assert_send_sync<T: Send + Sync>() {}
1912    const _: () = _assert_send_sync::<ServerVerifier>();
1913    const _: () = _assert_send_sync::<ClientVerifier>();
1914    const _: () = _assert_send_sync::<AnsVerifier>();
1915    const _: () = _assert_send_sync::<BadgeCache>();
1916
1917    fn create_test_badge(host: &str, version: &str, server_fp: &str, identity_fp: &str) -> Badge {
1918        serde_json::from_value(serde_json::json!({
1919            "status": "ACTIVE",
1920            "schemaVersion": "V1",
1921            "payload": {
1922                "logId": Uuid::new_v4().to_string(),
1923                "producer": {
1924                    "event": {
1925                        "ansId": Uuid::new_v4().to_string(),
1926                        "ansName": format!("ans://{version}.{host}"),
1927                        "eventType": "AGENT_REGISTERED",
1928                        "agent": { "host": host, "name": "Test Agent", "version": version },
1929                        "attestations": {
1930                            "domainValidation": "ACME-DNS-01",
1931                            "identityCert": { "fingerprint": identity_fp, "type": "X509-OV-CLIENT" },
1932                            "serverCert": { "fingerprint": server_fp, "type": "X509-DV-SERVER" }
1933                        },
1934                        "expiresAt": (Utc::now() + chrono::Duration::days(365)).to_rfc3339(),
1935                        "issuedAt": Utc::now().to_rfc3339(),
1936                        "raId": "test-ra",
1937                        "timestamp": Utc::now().to_rfc3339()
1938                    },
1939                    "keyId": "test-key",
1940                    "signature": "test-sig"
1941                }
1942            }
1943        })).expect("test badge JSON should be valid")
1944    }
1945
1946    fn create_test_cert_identity(cn: &str, fingerprint: &str) -> CertIdentity {
1947        CertIdentity {
1948            common_name: Some(cn.to_string()),
1949            dns_sans: vec![cn.to_string()],
1950            uri_sans: vec![],
1951            fingerprint: CertFingerprint::parse(fingerprint).unwrap(),
1952        }
1953    }
1954
1955    #[tokio::test]
1956    async fn test_server_verification_success() {
1957        let host = "test.example.com";
1958        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
1959
1960        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
1961        let badge_url = "https://tlog.example.com/v1/agents/test-id";
1962
1963        let dns_record = BadgeRecord {
1964            format_version: "ans-badge1".to_string(),
1965            version: Some(Version::new(1, 0, 0)),
1966            url: badge_url.to_string(),
1967        };
1968
1969        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
1970
1971        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
1972
1973        let verifier = ServerVerifier {
1974            dns_resolver,
1975            tlog_client,
1976            cache: None,
1977            failure_policy: FailurePolicy::FailClosed,
1978            dane_policy: DanePolicy::Disabled,
1979            dane_port: 443,
1980            trusted_ra_domains: None,
1981        };
1982
1983        let cert = create_test_cert_identity(host, fingerprint);
1984        let fqdn = Fqdn::new(host).unwrap();
1985
1986        let outcome = verifier.verify(&fqdn, &cert).await;
1987        assert!(outcome.is_success());
1988    }
1989
1990    #[tokio::test]
1991    async fn test_server_verification_not_ans_agent() {
1992        let dns_resolver = Arc::new(MockDnsResolver::new());
1993        let tlog_client = Arc::new(MockTransparencyLogClient::new());
1994
1995        let verifier = ServerVerifier {
1996            dns_resolver,
1997            tlog_client,
1998            cache: None,
1999            failure_policy: FailurePolicy::FailClosed,
2000            dane_policy: DanePolicy::Disabled,
2001            dane_port: 443,
2002            trusted_ra_domains: None,
2003        };
2004
2005        let cert = create_test_cert_identity(
2006            "unknown.example.com",
2007            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2008        );
2009        let fqdn = Fqdn::new("unknown.example.com").unwrap();
2010
2011        let outcome = verifier.verify(&fqdn, &cert).await;
2012        assert!(outcome.is_not_ans_agent());
2013    }
2014
2015    #[tokio::test]
2016    async fn test_server_verification_fingerprint_mismatch() {
2017        let host = "test.example.com";
2018        let badge_fingerprint =
2019            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2020        let cert_fingerprint =
2021            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2022
2023        let badge = create_test_badge(host, "v1.0.0", badge_fingerprint, "SHA256:aaa");
2024        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2025
2026        let dns_record = BadgeRecord {
2027            format_version: "ans-badge1".to_string(),
2028            version: Some(Version::new(1, 0, 0)),
2029            url: badge_url.to_string(),
2030        };
2031
2032        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2033
2034        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2035
2036        let verifier = ServerVerifier {
2037            dns_resolver,
2038            tlog_client,
2039            cache: None,
2040            failure_policy: FailurePolicy::FailClosed,
2041            dane_policy: DanePolicy::Disabled,
2042            dane_port: 443,
2043            trusted_ra_domains: None,
2044        };
2045
2046        let cert = create_test_cert_identity(host, cert_fingerprint);
2047        let fqdn = Fqdn::new(host).unwrap();
2048
2049        let outcome = verifier.verify(&fqdn, &cert).await;
2050        assert!(matches!(
2051            outcome,
2052            VerificationOutcome::FingerprintMismatch { .. }
2053        ));
2054    }
2055
2056    #[tokio::test]
2057    async fn test_server_verification_invalid_status() {
2058        let host = "test.example.com";
2059        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2060
2061        let mut badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2062        badge.status = BadgeStatus::Revoked;
2063
2064        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2065
2066        let dns_record = BadgeRecord {
2067            format_version: "ans-badge1".to_string(),
2068            version: Some(Version::new(1, 0, 0)),
2069            url: badge_url.to_string(),
2070        };
2071
2072        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2073
2074        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2075
2076        let verifier = ServerVerifier {
2077            dns_resolver,
2078            tlog_client,
2079            cache: None,
2080            failure_policy: FailurePolicy::FailClosed,
2081            dane_policy: DanePolicy::Disabled,
2082            dane_port: 443,
2083            trusted_ra_domains: None,
2084        };
2085
2086        let cert = create_test_cert_identity(host, fingerprint);
2087        let fqdn = Fqdn::new(host).unwrap();
2088
2089        let outcome = verifier.verify(&fqdn, &cert).await;
2090        assert!(matches!(
2091            outcome,
2092            VerificationOutcome::InvalidStatus {
2093                status: BadgeStatus::Revoked,
2094                ..
2095            }
2096        ));
2097    }
2098
2099    #[tokio::test]
2100    async fn test_verification_outcome_is_success() {
2101        let badge = create_test_badge(
2102            "test.example.com",
2103            "v1.0.0",
2104            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2105            "SHA256:aaa",
2106        );
2107
2108        let outcome = VerificationOutcome::Verified {
2109            badge,
2110            matched_fingerprint: CertFingerprint::parse(
2111                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2112            )
2113            .unwrap(),
2114        };
2115
2116        assert!(outcome.is_success());
2117        assert!(!outcome.is_not_ans_agent());
2118    }
2119
2120    #[tokio::test]
2121    async fn test_verification_with_cache() {
2122        let host = "test.example.com";
2123        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2124
2125        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2126        let cache = Arc::new(BadgeCache::with_defaults());
2127        let fqdn = Fqdn::new(host).unwrap();
2128
2129        // Pre-populate cache
2130        cache
2131            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), badge)
2132            .await;
2133
2134        // Create verifier with empty DNS/TLog (should use cache)
2135        let dns_resolver = Arc::new(MockDnsResolver::new());
2136        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2137
2138        let verifier = ServerVerifier {
2139            dns_resolver,
2140            tlog_client,
2141            cache: Some(cache),
2142            failure_policy: FailurePolicy::FailClosed,
2143            dane_policy: DanePolicy::Disabled,
2144            dane_port: 443,
2145            trusted_ra_domains: None,
2146        };
2147
2148        let cert = create_test_cert_identity(host, fingerprint);
2149
2150        let outcome = verifier.verify(&fqdn, &cert).await;
2151        assert!(outcome.is_success());
2152    }
2153
2154    #[test]
2155    fn test_cert_identity_from_components() {
2156        let fingerprint = CertFingerprint::parse(
2157            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2158        )
2159        .unwrap();
2160
2161        let identity = CertIdentity::new(
2162            Some("test.example.com".to_string()),
2163            vec!["test.example.com".to_string()],
2164            vec!["ans://v1.0.0.test.example.com".to_string()],
2165            fingerprint,
2166        );
2167
2168        assert_eq!(identity.fqdn(), Some("test.example.com"));
2169        assert!(identity.ans_name().is_some());
2170        assert_eq!(identity.version(), Some(Version::new(1, 0, 0)));
2171    }
2172
2173    #[test]
2174    fn test_cert_identity_from_fingerprint_and_cn() {
2175        let fingerprint = CertFingerprint::parse(
2176            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2177        )
2178        .unwrap();
2179
2180        let identity =
2181            CertIdentity::from_fingerprint_and_cn(fingerprint, "test.example.com".to_string());
2182
2183        assert_eq!(identity.fqdn(), Some("test.example.com"));
2184        assert!(identity.ans_name().is_none()); // No URI SANs
2185    }
2186
2187    // =========================================================================
2188    // ClientVerifier Tests
2189    // =========================================================================
2190
2191    fn create_mtls_cert_identity(host: &str, version: &str, fingerprint: &str) -> CertIdentity {
2192        CertIdentity {
2193            common_name: Some(host.to_string()),
2194            dns_sans: vec![host.to_string()],
2195            uri_sans: vec![format!("ans://{}.{}", version, host)],
2196            fingerprint: CertFingerprint::parse(fingerprint).unwrap(),
2197        }
2198    }
2199
2200    #[tokio::test]
2201    async fn test_client_verification_success() {
2202        let host = "test.example.com";
2203        let version = "v1.0.0";
2204        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
2205        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2206
2207        let badge = create_test_badge(host, version, server_fp, identity_fp);
2208        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2209
2210        let dns_record = BadgeRecord {
2211            format_version: "ans-badge1".to_string(),
2212            version: Some(Version::new(1, 0, 0)),
2213            url: badge_url.to_string(),
2214        };
2215
2216        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2217        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2218
2219        let verifier = ClientVerifier {
2220            dns_resolver,
2221            tlog_client,
2222            cache: None,
2223            failure_policy: FailurePolicy::FailClosed,
2224            trusted_ra_domains: None,
2225        };
2226
2227        let cert = create_mtls_cert_identity(host, version, identity_fp);
2228        let outcome = verifier.verify(&cert).await;
2229
2230        assert!(outcome.is_success(), "Expected success, got: {:?}", outcome);
2231    }
2232
2233    #[tokio::test]
2234    async fn test_client_verification_no_fqdn() {
2235        let dns_resolver = Arc::new(MockDnsResolver::new());
2236        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2237
2238        let verifier = ClientVerifier {
2239            dns_resolver,
2240            tlog_client,
2241            cache: None,
2242            failure_policy: FailurePolicy::FailClosed,
2243            trusted_ra_domains: None,
2244        };
2245
2246        // Create cert with no CN or DNS SANs
2247        let cert = CertIdentity {
2248            common_name: None,
2249            dns_sans: vec![],
2250            uri_sans: vec!["ans://v1.0.0.test.example.com".to_string()],
2251            fingerprint: CertFingerprint::parse(
2252                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2253            )
2254            .unwrap(),
2255        };
2256
2257        let outcome = verifier.verify(&cert).await;
2258        assert!(matches!(outcome, VerificationOutcome::CertError(_)));
2259    }
2260
2261    #[tokio::test]
2262    async fn test_client_verification_no_ans_name() {
2263        let dns_resolver = Arc::new(MockDnsResolver::new());
2264        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2265
2266        let verifier = ClientVerifier {
2267            dns_resolver,
2268            tlog_client,
2269            cache: None,
2270            failure_policy: FailurePolicy::FailClosed,
2271            trusted_ra_domains: None,
2272        };
2273
2274        // Create cert with CN but no URI SANs
2275        let cert = CertIdentity {
2276            common_name: Some("test.example.com".to_string()),
2277            dns_sans: vec!["test.example.com".to_string()],
2278            uri_sans: vec![],
2279            fingerprint: CertFingerprint::parse(
2280                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2281            )
2282            .unwrap(),
2283        };
2284
2285        let outcome = verifier.verify(&cert).await;
2286        assert!(matches!(outcome, VerificationOutcome::CertError(_)));
2287    }
2288
2289    #[tokio::test]
2290    async fn test_client_verification_fingerprint_mismatch() {
2291        let host = "test.example.com";
2292        let version = "v1.0.0";
2293        let badge_identity_fp =
2294            "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
2295        let cert_identity_fp =
2296            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2297
2298        let badge = create_test_badge(host, version, "SHA256:server", badge_identity_fp);
2299        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2300
2301        let dns_record = BadgeRecord {
2302            format_version: "ans-badge1".to_string(),
2303            version: Some(Version::new(1, 0, 0)),
2304            url: badge_url.to_string(),
2305        };
2306
2307        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2308        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2309
2310        let verifier = ClientVerifier {
2311            dns_resolver,
2312            tlog_client,
2313            cache: None,
2314            failure_policy: FailurePolicy::FailClosed,
2315            trusted_ra_domains: None,
2316        };
2317
2318        let cert = create_mtls_cert_identity(host, version, cert_identity_fp);
2319        let outcome = verifier.verify(&cert).await;
2320
2321        assert!(matches!(
2322            outcome,
2323            VerificationOutcome::FingerprintMismatch { .. }
2324        ));
2325    }
2326
2327    #[tokio::test]
2328    async fn test_client_verification_ans_name_mismatch() {
2329        let host = "test.example.com";
2330        let badge_version = "v1.0.0";
2331        let cert_version = "v2.0.0";
2332        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
2333
2334        // Badge has v1.0.0, cert has v2.0.0
2335        let badge = create_test_badge(host, badge_version, "SHA256:server", identity_fp);
2336        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2337
2338        let dns_record = BadgeRecord {
2339            format_version: "ans-badge1".to_string(),
2340            version: Some(Version::new(2, 0, 0)),
2341            url: badge_url.to_string(),
2342        };
2343
2344        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2345        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2346
2347        let verifier = ClientVerifier {
2348            dns_resolver,
2349            tlog_client,
2350            cache: None,
2351            failure_policy: FailurePolicy::FailClosed,
2352            trusted_ra_domains: None,
2353        };
2354
2355        let cert = create_mtls_cert_identity(host, cert_version, identity_fp);
2356        let outcome = verifier.verify(&cert).await;
2357
2358        assert!(matches!(
2359            outcome,
2360            VerificationOutcome::AnsNameMismatch { .. }
2361        ));
2362    }
2363
2364    // =========================================================================
2365    // VerificationOutcome Tests
2366    // =========================================================================
2367
2368    #[test]
2369    fn test_verification_outcome_badge() {
2370        let badge = create_test_badge(
2371            "test.example.com",
2372            "v1.0.0",
2373            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2374            "SHA256:aaa",
2375        );
2376
2377        // Verified has badge
2378        let outcome = VerificationOutcome::Verified {
2379            badge: badge.clone(),
2380            matched_fingerprint: CertFingerprint::parse(
2381                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2382            )
2383            .unwrap(),
2384        };
2385        assert!(outcome.badge().is_some());
2386
2387        // InvalidStatus has badge
2388        let outcome = VerificationOutcome::InvalidStatus {
2389            status: BadgeStatus::Revoked,
2390            badge: badge.clone(),
2391        };
2392        assert!(outcome.badge().is_some());
2393
2394        // FingerprintMismatch has badge
2395        let outcome = VerificationOutcome::FingerprintMismatch {
2396            expected: "SHA256:a".to_string(),
2397            actual: "SHA256:b".to_string(),
2398            badge: badge.clone(),
2399        };
2400        assert!(outcome.badge().is_some());
2401
2402        // HostnameMismatch has badge
2403        let outcome = VerificationOutcome::HostnameMismatch {
2404            expected: "a.com".to_string(),
2405            actual: "b.com".to_string(),
2406            badge: badge.clone(),
2407        };
2408        assert!(outcome.badge().is_some());
2409
2410        // AnsNameMismatch has badge
2411        let outcome = VerificationOutcome::AnsNameMismatch {
2412            expected: "ans://v1.0.0.a.com".to_string(),
2413            actual: "ans://v2.0.0.a.com".to_string(),
2414            badge,
2415        };
2416        assert!(outcome.badge().is_some());
2417
2418        // NotAnsAgent has no badge
2419        let outcome = VerificationOutcome::NotAnsAgent {
2420            fqdn: "test.com".to_string(),
2421        };
2422        assert!(outcome.badge().is_none());
2423
2424        // DnsError has no badge
2425        let outcome = VerificationOutcome::DnsError(DnsError::NotFound {
2426            fqdn: "test.com".to_string(),
2427        });
2428        assert!(outcome.badge().is_none());
2429    }
2430
2431    #[test]
2432    fn test_verification_outcome_into_result() {
2433        let badge = create_test_badge(
2434            "test.example.com",
2435            "v1.0.0",
2436            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2437            "SHA256:aaa",
2438        );
2439
2440        // Verified -> Ok
2441        let outcome = VerificationOutcome::Verified {
2442            badge: badge.clone(),
2443            matched_fingerprint: CertFingerprint::parse(
2444                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2445            )
2446            .unwrap(),
2447        };
2448        assert!(outcome.into_result().is_ok());
2449
2450        // NotAnsAgent -> Err
2451        let outcome = VerificationOutcome::NotAnsAgent {
2452            fqdn: "test.com".to_string(),
2453        };
2454        assert!(outcome.into_result().is_err());
2455
2456        // InvalidStatus -> Err
2457        let outcome = VerificationOutcome::InvalidStatus {
2458            status: BadgeStatus::Revoked,
2459            badge: badge.clone(),
2460        };
2461        assert!(outcome.into_result().is_err());
2462
2463        // FingerprintMismatch -> Err
2464        let outcome = VerificationOutcome::FingerprintMismatch {
2465            expected: "a".to_string(),
2466            actual: "b".to_string(),
2467            badge: badge.clone(),
2468        };
2469        assert!(outcome.into_result().is_err());
2470
2471        // HostnameMismatch -> Err
2472        let outcome = VerificationOutcome::HostnameMismatch {
2473            expected: "a.com".to_string(),
2474            actual: "b.com".to_string(),
2475            badge: badge.clone(),
2476        };
2477        assert!(outcome.into_result().is_err());
2478
2479        // AnsNameMismatch -> Err
2480        let outcome = VerificationOutcome::AnsNameMismatch {
2481            expected: "a".to_string(),
2482            actual: "b".to_string(),
2483            badge,
2484        };
2485        assert!(outcome.into_result().is_err());
2486
2487        // DnsError -> Err
2488        let outcome = VerificationOutcome::DnsError(DnsError::NotFound {
2489            fqdn: "test.com".to_string(),
2490        });
2491        assert!(outcome.into_result().is_err());
2492
2493        // TlogError -> Err
2494        let outcome = VerificationOutcome::TlogError(TlogError::ServiceUnavailable);
2495        assert!(outcome.into_result().is_err());
2496
2497        // DaneError -> Err
2498        let outcome = VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
2499        assert!(outcome.into_result().is_err());
2500    }
2501
2502    // =========================================================================
2503    // Hostname Mismatch Tests
2504    // =========================================================================
2505
2506    #[tokio::test]
2507    async fn test_server_verification_hostname_mismatch() {
2508        let badge_host = "badge.example.com";
2509        let cert_host = "different.example.com";
2510        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2511
2512        let badge = create_test_badge(badge_host, "v1.0.0", fingerprint, "SHA256:aaa");
2513        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2514
2515        let dns_record = BadgeRecord {
2516            format_version: "ans-badge1".to_string(),
2517            version: Some(Version::new(1, 0, 0)),
2518            url: badge_url.to_string(),
2519        };
2520
2521        // DNS lookup uses cert_host but badge contains badge_host
2522        let dns_resolver =
2523            Arc::new(MockDnsResolver::new().with_records(cert_host, vec![dns_record]));
2524        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2525
2526        let verifier = ServerVerifier {
2527            dns_resolver,
2528            tlog_client,
2529            cache: None,
2530            failure_policy: FailurePolicy::FailClosed,
2531            dane_policy: DanePolicy::Disabled,
2532            dane_port: 443,
2533            trusted_ra_domains: None,
2534        };
2535
2536        let cert = create_test_cert_identity(cert_host, fingerprint);
2537        let fqdn = Fqdn::new(cert_host).unwrap();
2538
2539        let outcome = verifier.verify(&fqdn, &cert).await;
2540        assert!(
2541            matches!(outcome, VerificationOutcome::HostnameMismatch { .. }),
2542            "Expected HostnameMismatch, got: {:?}",
2543            outcome
2544        );
2545    }
2546
2547    // =========================================================================
2548    // Prefetch Tests
2549    // =========================================================================
2550
2551    #[tokio::test]
2552    async fn test_server_verifier_prefetch_success() {
2553        let host = "test.example.com";
2554        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2555
2556        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2557        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2558
2559        let dns_record = BadgeRecord {
2560            format_version: "ans-badge1".to_string(),
2561            version: Some(Version::new(1, 0, 0)),
2562            url: badge_url.to_string(),
2563        };
2564
2565        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2566        let tlog_client =
2567            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge.clone()));
2568
2569        let verifier = ServerVerifier {
2570            dns_resolver,
2571            tlog_client,
2572            cache: Some(Arc::new(BadgeCache::with_defaults())),
2573            failure_policy: FailurePolicy::FailClosed,
2574            dane_policy: DanePolicy::Disabled,
2575            dane_port: 443,
2576            trusted_ra_domains: None,
2577        };
2578
2579        let fqdn = Fqdn::new(host).unwrap();
2580        let result = verifier.prefetch(&fqdn).await;
2581
2582        assert!(result.is_ok());
2583        assert_eq!(result.unwrap().agent_host(), host);
2584    }
2585
2586    #[tokio::test]
2587    async fn test_server_verifier_prefetch_not_found() {
2588        let dns_resolver = Arc::new(MockDnsResolver::new());
2589        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2590
2591        let verifier = ServerVerifier {
2592            dns_resolver,
2593            tlog_client,
2594            cache: None,
2595            failure_policy: FailurePolicy::FailClosed,
2596            dane_policy: DanePolicy::Disabled,
2597            dane_port: 443,
2598            trusted_ra_domains: None,
2599        };
2600
2601        let fqdn = Fqdn::new("unknown.example.com").unwrap();
2602        let result = verifier.prefetch(&fqdn).await;
2603
2604        assert!(result.is_err());
2605        assert!(matches!(result.unwrap_err(), AnsError::Dns(_)));
2606    }
2607
2608    // =========================================================================
2609    // FailurePolicy Tests
2610    // =========================================================================
2611
2612    #[tokio::test]
2613    async fn test_failure_policy_fail_open_with_cache_no_cache() {
2614        let dns_resolver = Arc::new(MockDnsResolver::new().with_error(
2615            "test.example.com",
2616            DnsError::LookupFailed {
2617                fqdn: "test.example.com".to_string(),
2618                reason: "timeout".to_string(),
2619            },
2620        ));
2621        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2622
2623        let verifier = ServerVerifier {
2624            dns_resolver,
2625            tlog_client,
2626            cache: Some(Arc::new(BadgeCache::with_defaults())),
2627            failure_policy: FailurePolicy::FailOpenWithCache {
2628                max_staleness: Duration::from_secs(600),
2629            },
2630            dane_policy: DanePolicy::Disabled,
2631            dane_port: 443,
2632            trusted_ra_domains: None,
2633        };
2634
2635        let cert = create_test_cert_identity(
2636            "test.example.com",
2637            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2638        );
2639        let fqdn = Fqdn::new("test.example.com").unwrap();
2640
2641        let outcome = verifier.verify(&fqdn, &cert).await;
2642        // No cached badge, so returns DNS error
2643        assert!(matches!(outcome, VerificationOutcome::DnsError(_)));
2644    }
2645
2646    #[tokio::test]
2647    async fn test_failure_policy_fail_open_with_cache_uses_cache() {
2648        let host = "test.example.com";
2649        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2650
2651        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2652        let cache = Arc::new(BadgeCache::with_defaults());
2653        let fqdn = Fqdn::new(host).unwrap();
2654
2655        // Pre-populate cache
2656        cache
2657            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), badge)
2658            .await;
2659
2660        let dns_resolver = Arc::new(MockDnsResolver::new().with_error(
2661            host,
2662            DnsError::LookupFailed {
2663                fqdn: host.to_string(),
2664                reason: "timeout".to_string(),
2665            },
2666        ));
2667        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2668
2669        let verifier = ServerVerifier {
2670            dns_resolver,
2671            tlog_client,
2672            cache: Some(cache),
2673            failure_policy: FailurePolicy::FailOpenWithCache {
2674                max_staleness: Duration::from_secs(600),
2675            },
2676            dane_policy: DanePolicy::Disabled,
2677            dane_port: 443,
2678            trusted_ra_domains: None,
2679        };
2680
2681        let cert = create_test_cert_identity(host, fingerprint);
2682
2683        let outcome = verifier.verify(&fqdn, &cert).await;
2684        // Should use cached badge and verify successfully
2685        assert!(
2686            outcome.is_success(),
2687            "Expected success with cache, got: {:?}",
2688            outcome
2689        );
2690    }
2691
2692    #[test]
2693    fn test_cert_identity_from_der_server_cert() {
2694        use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType};
2695
2696        // Generate a server certificate on the fly
2697        let key_pair = KeyPair::generate().unwrap();
2698        let mut params = CertificateParams::default();
2699        params
2700            .distinguished_name
2701            .push(DnType::CommonName, "test.agent.local");
2702        params.subject_alt_names.push(SanType::DnsName(
2703            "test.agent.local".to_string().try_into().unwrap(),
2704        ));
2705        params.subject_alt_names.push(SanType::URI(
2706            "ans://v1.0.0.test.agent.local".try_into().unwrap(),
2707        ));
2708        params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
2709
2710        let cert = params.self_signed(&key_pair).unwrap();
2711        let der = cert.der();
2712
2713        let identity = CertIdentity::from_der(der).expect("should parse DER certificate");
2714
2715        // Verify CN
2716        assert_eq!(
2717            identity.common_name.as_deref(),
2718            Some("test.agent.local"),
2719            "CN should be test.agent.local"
2720        );
2721
2722        // Verify DNS SAN
2723        assert!(
2724            identity.dns_sans.contains(&"test.agent.local".to_string()),
2725            "DNS SANs should contain test.agent.local, got: {:?}",
2726            identity.dns_sans
2727        );
2728
2729        // Verify URI SAN (ANS name)
2730        assert!(
2731            identity
2732                .uri_sans
2733                .contains(&"ans://v1.0.0.test.agent.local".to_string()),
2734            "URI SANs should contain ans://v1.0.0.test.agent.local, got: {:?}",
2735            identity.uri_sans
2736        );
2737
2738        // Verify fingerprint matches what we compute from the same DER bytes
2739        let expected_fp = CertFingerprint::from_der(der);
2740        assert_eq!(
2741            identity.fingerprint, expected_fp,
2742            "Fingerprint should match computed fingerprint from same DER"
2743        );
2744
2745        // Verify convenience methods
2746        assert_eq!(identity.fqdn(), Some("test.agent.local"));
2747        let ans_name = identity.ans_name().expect("should have ANS name");
2748        assert_eq!(ans_name.fqdn().as_str(), "test.agent.local");
2749        assert_eq!(identity.version(), Some(Version::new(1, 0, 0)));
2750    }
2751
2752    #[test]
2753    fn test_cert_identity_from_der_client_cert() {
2754        use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType};
2755
2756        // Generate a client (identity) certificate on the fly
2757        let key_pair = KeyPair::generate().unwrap();
2758        let mut params = CertificateParams::default();
2759        params
2760            .distinguished_name
2761            .push(DnType::CommonName, "test.agent.local");
2762        params.subject_alt_names.push(SanType::DnsName(
2763            "test.agent.local".to_string().try_into().unwrap(),
2764        ));
2765        params.subject_alt_names.push(SanType::URI(
2766            "ans://v1.0.0.test.agent.local".try_into().unwrap(),
2767        ));
2768        params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
2769
2770        let cert = params.self_signed(&key_pair).unwrap();
2771        let der = cert.der();
2772
2773        let identity = CertIdentity::from_der(der).expect("should parse DER certificate");
2774
2775        assert_eq!(identity.common_name.as_deref(), Some("test.agent.local"));
2776        assert!(identity.dns_sans.contains(&"test.agent.local".to_string()));
2777        assert!(
2778            identity
2779                .uri_sans
2780                .contains(&"ans://v1.0.0.test.agent.local".to_string())
2781        );
2782
2783        let expected_fp = CertFingerprint::from_der(der);
2784        assert_eq!(identity.fingerprint, expected_fp);
2785    }
2786
2787    #[test]
2788    fn test_cert_identity_from_der_invalid_bytes() {
2789        let result = CertIdentity::from_der(b"not a certificate");
2790        assert!(result.is_err(), "Should fail on invalid DER bytes");
2791    }
2792
2793    #[tokio::test]
2794    async fn test_server_verifier_builder_dane_policy() {
2795        let dns = Arc::new(MockDnsResolver::new());
2796        let tlog = Arc::new(MockTransparencyLogClient::new());
2797
2798        // with_dane_if_present convenience method
2799        let verifier = ServerVerifier::builder()
2800            .dns_resolver(dns.clone())
2801            .tlog_client(tlog.clone())
2802            .with_dane_if_present()
2803            .build()
2804            .await
2805            .unwrap();
2806        assert_eq!(verifier.dane_policy, DanePolicy::ValidateIfPresent);
2807
2808        // require_dane convenience method
2809        let verifier = ServerVerifier::builder()
2810            .dns_resolver(dns.clone())
2811            .tlog_client(tlog.clone())
2812            .require_dane()
2813            .build()
2814            .await
2815            .unwrap();
2816        assert_eq!(verifier.dane_policy, DanePolicy::Required);
2817
2818        // explicit dane_policy
2819        let verifier = ServerVerifier::builder()
2820            .dns_resolver(dns.clone())
2821            .tlog_client(tlog.clone())
2822            .dane_policy(DanePolicy::Disabled)
2823            .build()
2824            .await
2825            .unwrap();
2826        assert_eq!(verifier.dane_policy, DanePolicy::Disabled);
2827    }
2828
2829    #[tokio::test]
2830    async fn test_server_verifier_builder_dane_port() {
2831        let dns = Arc::new(MockDnsResolver::new());
2832        let tlog = Arc::new(MockTransparencyLogClient::new());
2833
2834        // Default port is 443
2835        let verifier = ServerVerifier::builder()
2836            .dns_resolver(dns.clone())
2837            .tlog_client(tlog.clone())
2838            .build()
2839            .await
2840            .unwrap();
2841        assert_eq!(verifier.dane_port, 443);
2842
2843        // Custom port
2844        let verifier = ServerVerifier::builder()
2845            .dns_resolver(dns.clone())
2846            .tlog_client(tlog.clone())
2847            .dane_port(8443)
2848            .build()
2849            .await
2850            .unwrap();
2851        assert_eq!(verifier.dane_port, 8443);
2852    }
2853
2854    #[tokio::test]
2855    async fn test_server_verifier_builder_failure_policy() {
2856        let dns = Arc::new(MockDnsResolver::new());
2857        let tlog = Arc::new(MockTransparencyLogClient::new());
2858
2859        let verifier = ServerVerifier::builder()
2860            .dns_resolver(dns)
2861            .tlog_client(tlog)
2862            .failure_policy(FailurePolicy::FailClosed)
2863            .build()
2864            .await
2865            .unwrap();
2866        assert!(matches!(verifier.failure_policy, FailurePolicy::FailClosed));
2867    }
2868
2869    // =========================================================================
2870    // Refresh-on-Mismatch Tests
2871    // =========================================================================
2872
2873    #[tokio::test]
2874    async fn test_server_verification_refresh_on_mismatch_succeeds() {
2875        let host = "test.example.com";
2876        let old_fp = "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2877        let new_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2878
2879        // First badge has old fingerprint (will mismatch)
2880        // After refresh, tlog returns badge with new fingerprint
2881        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2882        let updated_badge = create_test_badge(host, "v1.0.0", new_fp, "SHA256:aaa");
2883
2884        let dns_record = BadgeRecord {
2885            format_version: "ans-badge1".to_string(),
2886            version: Some(Version::new(1, 0, 0)),
2887            url: badge_url.to_string(),
2888        };
2889
2890        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2891        // Mock always returns updated badge (simulates tlog updated after cert renewal)
2892        let tlog_client =
2893            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, updated_badge));
2894
2895        let cache = Arc::new(BadgeCache::with_defaults());
2896        let fqdn = Fqdn::new(host).unwrap();
2897
2898        // Pre-populate cache with stale badge (old fingerprint)
2899        let stale_badge = create_test_badge(host, "v1.0.0", old_fp, "SHA256:aaa");
2900        cache
2901            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), stale_badge)
2902            .await;
2903
2904        let verifier = ServerVerifier {
2905            dns_resolver,
2906            tlog_client,
2907            cache: Some(cache),
2908            failure_policy: FailurePolicy::FailClosed,
2909            dane_policy: DanePolicy::Disabled,
2910            dane_port: 443,
2911            trusted_ra_domains: None,
2912        };
2913
2914        // Cert has the NEW fingerprint — cache has OLD → mismatch → refresh → success
2915        let cert = create_test_cert_identity(host, new_fp);
2916        let outcome = verifier.verify(&fqdn, &cert).await;
2917        assert!(
2918            outcome.is_success(),
2919            "Expected success after refresh, got: {:?}",
2920            outcome
2921        );
2922    }
2923
2924    #[tokio::test]
2925    async fn test_server_verification_refresh_on_mismatch_still_fails() {
2926        let host = "test.example.com";
2927        let badge_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2928        let cert_fp = "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2929
2930        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2931        // Badge always has badge_fp, cert always has cert_fp — never matches
2932        let badge = create_test_badge(host, "v1.0.0", badge_fp, "SHA256:aaa");
2933
2934        let dns_record = BadgeRecord {
2935            format_version: "ans-badge1".to_string(),
2936            version: Some(Version::new(1, 0, 0)),
2937            url: badge_url.to_string(),
2938        };
2939
2940        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2941        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2942
2943        let verifier = ServerVerifier {
2944            dns_resolver,
2945            tlog_client,
2946            cache: None,
2947            failure_policy: FailurePolicy::FailClosed,
2948            dane_policy: DanePolicy::Disabled,
2949            dane_port: 443,
2950            trusted_ra_domains: None,
2951        };
2952
2953        let cert = create_test_cert_identity(host, cert_fp);
2954        let fqdn = Fqdn::new(host).unwrap();
2955
2956        let outcome = verifier.verify(&fqdn, &cert).await;
2957        assert!(
2958            matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }),
2959            "Expected FingerprintMismatch after refresh still fails, got: {:?}",
2960            outcome
2961        );
2962    }
2963
2964    #[tokio::test]
2965    async fn test_client_verification_refresh_on_mismatch_succeeds() {
2966        let host = "test.example.com";
2967        let version = "v1.0.0";
2968        let old_identity_fp =
2969            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2970        let new_identity_fp =
2971            "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
2972        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2973
2974        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2975        // Tlog returns updated badge with new identity fingerprint
2976        let updated_badge = create_test_badge(host, version, server_fp, new_identity_fp);
2977
2978        let dns_record = BadgeRecord {
2979            format_version: "ans-badge1".to_string(),
2980            version: Some(Version::new(1, 0, 0)),
2981            url: badge_url.to_string(),
2982        };
2983
2984        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2985        let tlog_client =
2986            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, updated_badge));
2987
2988        let cache = Arc::new(BadgeCache::with_defaults());
2989        let fqdn = Fqdn::new(host).unwrap();
2990        let ver = Version::new(1, 0, 0);
2991
2992        // Pre-populate cache with stale badge (old identity fingerprint)
2993        let stale_badge = create_test_badge(host, version, server_fp, old_identity_fp);
2994        cache
2995            .insert_for_fqdn_version(&fqdn, &ver, stale_badge)
2996            .await;
2997
2998        let verifier = ClientVerifier {
2999            dns_resolver,
3000            tlog_client,
3001            cache: Some(cache),
3002            failure_policy: FailurePolicy::FailClosed,
3003            trusted_ra_domains: None,
3004        };
3005
3006        // Client cert has new fingerprint — cache has old → mismatch → refresh → success
3007        let cert = create_mtls_cert_identity(host, version, new_identity_fp);
3008        let outcome = verifier.verify(&cert).await;
3009        assert!(
3010            outcome.is_success(),
3011            "Expected success after client refresh, got: {:?}",
3012            outcome
3013        );
3014    }
3015
3016    // =========================================================================
3017    // Trusted RA Domain Validation Tests
3018    // =========================================================================
3019
3020    #[test]
3021    fn test_validate_badge_domain_unit_allows_when_none() {
3022        assert!(validate_badge_domain(None, "https://tlog.example.com/v1/agents/test").is_ok());
3023    }
3024
3025    #[test]
3026    fn test_validate_badge_domain_unit_allows_trusted() {
3027        let trusted: HashSet<String> = ["tlog.example.com".to_string()].into();
3028        assert!(
3029            validate_badge_domain(Some(&trusted), "https://tlog.example.com/v1/agents/test")
3030                .is_ok()
3031        );
3032    }
3033
3034    #[test]
3035    fn test_validate_badge_domain_unit_rejects_untrusted() {
3036        let trusted: HashSet<String> = ["tlog.example.com".to_string()].into();
3037        let err = validate_badge_domain(Some(&trusted), "https://evil.attacker.com/v1/agents/test")
3038            .unwrap_err();
3039        assert!(
3040            matches!(err, TlogError::UntrustedDomain { domain, .. } if domain == "evil.attacker.com")
3041        );
3042    }
3043
3044    #[test]
3045    fn test_validate_badge_domain_unit_multiple_trusted() {
3046        let trusted: HashSet<String> = [
3047            "tlog1.example.com".to_string(),
3048            "tlog2.example.com".to_string(),
3049        ]
3050        .into();
3051        assert!(validate_badge_domain(Some(&trusted), "https://tlog1.example.com/badge").is_ok());
3052        assert!(validate_badge_domain(Some(&trusted), "https://tlog2.example.com/badge").is_ok());
3053        assert!(validate_badge_domain(Some(&trusted), "https://tlog3.example.com/badge").is_err());
3054    }
3055
3056    #[tokio::test]
3057    async fn test_trusted_ra_none_allows_all() {
3058        let host = "test.example.com";
3059        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3060        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3061        let badge_url = "https://any-domain.example.com/v1/agents/test-id";
3062
3063        let dns_record = BadgeRecord {
3064            format_version: "ans-badge1".to_string(),
3065            version: Some(Version::new(1, 0, 0)),
3066            url: badge_url.to_string(),
3067        };
3068        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3069        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3070
3071        let verifier = ServerVerifier {
3072            dns_resolver,
3073            tlog_client,
3074            cache: None,
3075            failure_policy: FailurePolicy::FailClosed,
3076            dane_policy: DanePolicy::Disabled,
3077            dane_port: 443,
3078            trusted_ra_domains: None,
3079        };
3080
3081        let cert = create_test_cert_identity(host, fingerprint);
3082        let fqdn = Fqdn::new(host).unwrap();
3083        let outcome = verifier.verify(&fqdn, &cert).await;
3084        assert!(outcome.is_success(), "None should allow all domains");
3085    }
3086
3087    #[tokio::test]
3088    async fn test_trusted_ra_allows_trusted_domain() {
3089        let host = "test.example.com";
3090        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3091        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3092        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3093
3094        let dns_record = BadgeRecord {
3095            format_version: "ans-badge1".to_string(),
3096            version: Some(Version::new(1, 0, 0)),
3097            url: badge_url.to_string(),
3098        };
3099        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3100        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3101
3102        let verifier = ServerVerifier {
3103            dns_resolver,
3104            tlog_client,
3105            cache: None,
3106            failure_policy: FailurePolicy::FailClosed,
3107            dane_policy: DanePolicy::Disabled,
3108            dane_port: 443,
3109            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3110        };
3111
3112        let cert = create_test_cert_identity(host, fingerprint);
3113        let fqdn = Fqdn::new(host).unwrap();
3114        let outcome = verifier.verify(&fqdn, &cert).await;
3115        assert!(outcome.is_success(), "Trusted domain should succeed");
3116    }
3117
3118    #[tokio::test]
3119    async fn test_trusted_ra_rejects_untrusted_domain() {
3120        let host = "test.example.com";
3121        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3122        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3123        let badge_url = "https://evil.attacker.com/v1/agents/test-id";
3124
3125        let dns_record = BadgeRecord {
3126            format_version: "ans-badge1".to_string(),
3127            version: Some(Version::new(1, 0, 0)),
3128            url: badge_url.to_string(),
3129        };
3130        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3131        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3132
3133        let verifier = ServerVerifier {
3134            dns_resolver,
3135            tlog_client,
3136            cache: None,
3137            failure_policy: FailurePolicy::FailClosed,
3138            dane_policy: DanePolicy::Disabled,
3139            dane_port: 443,
3140            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3141        };
3142
3143        let cert = create_test_cert_identity(host, fingerprint);
3144        let fqdn = Fqdn::new(host).unwrap();
3145        let outcome = verifier.verify(&fqdn, &cert).await;
3146        assert!(
3147            matches!(
3148                outcome,
3149                VerificationOutcome::TlogError(TlogError::UntrustedDomain { .. })
3150            ),
3151            "Untrusted domain should be rejected, got: {:?}",
3152            outcome
3153        );
3154    }
3155
3156    #[tokio::test]
3157    async fn test_trusted_ra_client_rejects_untrusted() {
3158        let host = "test.example.com";
3159        let version = "v1.0.0";
3160        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
3161        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3162        let badge = create_test_badge(host, version, server_fp, identity_fp);
3163        let badge_url = "https://evil.attacker.com/v1/agents/test-id";
3164
3165        let dns_record = BadgeRecord {
3166            format_version: "ans-badge1".to_string(),
3167            version: Some(Version::new(1, 0, 0)),
3168            url: badge_url.to_string(),
3169        };
3170        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3171        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3172
3173        let verifier = ClientVerifier {
3174            dns_resolver,
3175            tlog_client,
3176            cache: None,
3177            failure_policy: FailurePolicy::FailClosed,
3178            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3179        };
3180
3181        let cert = create_mtls_cert_identity(host, version, identity_fp);
3182        let outcome = verifier.verify(&cert).await;
3183        assert!(
3184            matches!(
3185                outcome,
3186                VerificationOutcome::TlogError(TlogError::UntrustedDomain { .. })
3187            ),
3188            "Client verifier should reject untrusted domain, got: {:?}",
3189            outcome
3190        );
3191    }
3192
3193    #[tokio::test]
3194    async fn test_trusted_ra_builder_propagation() {
3195        let dns_resolver = Arc::new(MockDnsResolver::new());
3196        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3197
3198        let verifier = ServerVerifier::builder()
3199            .dns_resolver(dns_resolver as Arc<dyn DnsResolver>)
3200            .tlog_client(tlog_client as Arc<dyn TransparencyLogClient>)
3201            .trusted_ra_domains(["tlog.example.com", "tlog2.example.com"])
3202            .build()
3203            .await
3204            .unwrap();
3205
3206        // Verify the builder propagated the trusted domains correctly
3207        let trusted = verifier.trusted_ra_domains.as_ref().unwrap();
3208        assert!(trusted.contains("tlog.example.com"));
3209        assert!(trusted.contains("tlog2.example.com"));
3210        assert_eq!(trusted.len(), 2);
3211    }
3212
3213    // =========================================================================
3214    // 7a: VerificationOutcome::into_result() — CertError and ParseError branches
3215    // =========================================================================
3216
3217    #[test]
3218    fn test_outcome_into_result_cert_error() {
3219        let outcome =
3220            VerificationOutcome::CertError(CryptoError::ParseFailed("bad cert".to_string()));
3221        let err = outcome.into_result().unwrap_err();
3222        assert!(matches!(err, AnsError::Certificate(_)));
3223    }
3224
3225    #[test]
3226    fn test_outcome_into_result_parse_error() {
3227        let outcome = VerificationOutcome::ParseError(ans_types::ParseError::InvalidFqdn(
3228            "bad fqdn".to_string(),
3229        ));
3230        let err = outcome.into_result().unwrap_err();
3231        assert!(matches!(err, AnsError::Parse(_)));
3232    }
3233
3234    #[test]
3235    fn test_outcome_into_result_dane_error() {
3236        let outcome = VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
3237        let err = outcome.into_result().unwrap_err();
3238        assert!(matches!(
3239            err,
3240            AnsError::Verification(VerificationError::DaneVerificationFailed(_))
3241        ));
3242    }
3243
3244    #[test]
3245    fn test_outcome_into_result_dns_error() {
3246        let outcome = VerificationOutcome::DnsError(DnsError::Timeout {
3247            fqdn: "test.example.com".to_string(),
3248        });
3249        let err = outcome.into_result().unwrap_err();
3250        assert!(matches!(err, AnsError::Dns(DnsError::Timeout { .. })));
3251    }
3252
3253    #[test]
3254    fn test_outcome_into_result_tlog_error() {
3255        let outcome = VerificationOutcome::TlogError(TlogError::ServiceUnavailable);
3256        let err = outcome.into_result().unwrap_err();
3257        assert!(matches!(
3258            err,
3259            AnsError::TransparencyLog(TlogError::ServiceUnavailable)
3260        ));
3261    }
3262
3263    // =========================================================================
3264    // 7b: AnsVerifierBuilder DNS presets
3265    // =========================================================================
3266
3267    #[tokio::test]
3268    async fn test_builder_dns_cloudflare() {
3269        let dns = Arc::new(MockDnsResolver::new());
3270        let tlog = Arc::new(MockTransparencyLogClient::new());
3271
3272        // Test that the builder method configures correctly
3273        let verifier = AnsVerifier::builder()
3274            .dns_resolver(dns as Arc<dyn DnsResolver>)
3275            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3276            .dns_cloudflare() // preset is ignored when custom resolver is set
3277            .build()
3278            .await
3279            .unwrap();
3280
3281        // Verify the builder produces a working verifier
3282        let dbg = format!("{verifier:?}");
3283        assert!(dbg.contains("AnsVerifier"));
3284    }
3285
3286    #[tokio::test]
3287    async fn test_builder_dns_nameservers() {
3288        let tlog = Arc::new(MockTransparencyLogClient::new());
3289
3290        let verifier = AnsVerifier::builder()
3291            .dns_nameservers(&[std::net::Ipv4Addr::new(1, 1, 1, 1)])
3292            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3293            .build()
3294            .await
3295            .unwrap();
3296
3297        let dbg = format!("{verifier:?}");
3298        assert!(dbg.contains("AnsVerifier"));
3299    }
3300
3301    #[tokio::test]
3302    async fn test_builder_dns_preset_path() {
3303        let tlog = Arc::new(MockTransparencyLogClient::new());
3304
3305        let verifier = AnsVerifier::builder()
3306            .dns_preset(DnsResolverConfig::Cloudflare)
3307            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3308            .build()
3309            .await
3310            .unwrap();
3311
3312        let dbg = format!("{verifier:?}");
3313        assert!(dbg.contains("AnsVerifier"));
3314    }
3315
3316    // =========================================================================
3317    // 7c: AnsVerifier rustls methods
3318    // =========================================================================
3319
3320    #[cfg(feature = "rustls")]
3321    #[tokio::test]
3322    async fn test_client_cert_verifier_without_pem() {
3323        let _ = rustls::crypto::ring::default_provider().install_default();
3324        let dns = Arc::new(MockDnsResolver::new());
3325        let tlog = Arc::new(MockTransparencyLogClient::new());
3326
3327        let verifier = AnsVerifier::builder()
3328            .dns_resolver(dns as Arc<dyn DnsResolver>)
3329            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3330            .build()
3331            .await
3332            .unwrap();
3333
3334        let result = verifier.client_cert_verifier();
3335        assert!(result.is_err());
3336    }
3337
3338    #[cfg(feature = "rustls")]
3339    #[tokio::test]
3340    async fn test_client_cert_verifier_with_pem() {
3341        let _ = rustls::crypto::ring::default_provider().install_default();
3342        let ca = rcgen::generate_simple_self_signed(vec!["ANS Test CA".to_string()]).unwrap();
3343        let ca_pem = ca.cert.pem();
3344
3345        let dns = Arc::new(MockDnsResolver::new());
3346        let tlog = Arc::new(MockTransparencyLogClient::new());
3347
3348        let verifier = AnsVerifier::builder()
3349            .dns_resolver(dns as Arc<dyn DnsResolver>)
3350            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3351            .private_ca_pem(ca_pem.as_bytes().to_vec())
3352            .build()
3353            .await
3354            .unwrap();
3355
3356        let cv = verifier.client_cert_verifier().unwrap();
3357        assert!(cv.requires_client_cert());
3358    }
3359
3360    #[cfg(feature = "rustls")]
3361    #[tokio::test]
3362    async fn test_client_cert_verifier_optional_with_pem() {
3363        let _ = rustls::crypto::ring::default_provider().install_default();
3364        let ca = rcgen::generate_simple_self_signed(vec!["ANS Test CA".to_string()]).unwrap();
3365        let ca_pem = ca.cert.pem();
3366
3367        let dns = Arc::new(MockDnsResolver::new());
3368        let tlog = Arc::new(MockTransparencyLogClient::new());
3369
3370        let verifier = AnsVerifier::builder()
3371            .dns_resolver(dns as Arc<dyn DnsResolver>)
3372            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3373            .private_ca_pem(ca_pem.as_bytes().to_vec())
3374            .build()
3375            .await
3376            .unwrap();
3377
3378        let cv = verifier.client_cert_verifier_optional().unwrap();
3379        assert!(!cv.requires_client_cert());
3380    }
3381
3382    #[cfg(feature = "rustls")]
3383    #[tokio::test]
3384    async fn test_server_cert_verifier() {
3385        let _ = rustls::crypto::ring::default_provider().install_default();
3386        let dns = Arc::new(MockDnsResolver::new());
3387        let tlog = Arc::new(MockTransparencyLogClient::new());
3388
3389        let verifier = AnsVerifier::builder()
3390            .dns_resolver(dns as Arc<dyn DnsResolver>)
3391            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3392            .build()
3393            .await
3394            .unwrap();
3395
3396        let fp = CertFingerprint::parse(
3397            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3398        )
3399        .unwrap();
3400        let sv = verifier.server_cert_verifier(&fp).unwrap();
3401        assert_eq!(sv.expected_fingerprint(), &fp);
3402    }
3403
3404    // =========================================================================
3405    // 7d: Builder configuration methods
3406    // =========================================================================
3407
3408    #[tokio::test]
3409    async fn test_builder_with_caching() {
3410        let dns = Arc::new(MockDnsResolver::new());
3411        let tlog = Arc::new(MockTransparencyLogClient::new());
3412
3413        let verifier = AnsVerifier::builder()
3414            .dns_resolver(dns as Arc<dyn DnsResolver>)
3415            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3416            .with_caching()
3417            .build()
3418            .await
3419            .unwrap();
3420
3421        // Verify the verifier was built with caching
3422        assert!(format!("{verifier:?}").contains("has_cache"));
3423    }
3424
3425    #[tokio::test]
3426    async fn test_builder_with_cache_config() {
3427        let dns = Arc::new(MockDnsResolver::new());
3428        let tlog = Arc::new(MockTransparencyLogClient::new());
3429
3430        let verifier = AnsVerifier::builder()
3431            .dns_resolver(dns as Arc<dyn DnsResolver>)
3432            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3433            .with_cache_config(CacheConfig::default())
3434            .build()
3435            .await
3436            .unwrap();
3437
3438        assert!(format!("{verifier:?}").contains("AnsVerifier"));
3439    }
3440
3441    #[tokio::test]
3442    async fn test_builder_with_dane_if_present() {
3443        let dns = Arc::new(MockDnsResolver::new());
3444        let tlog = Arc::new(MockTransparencyLogClient::new());
3445
3446        let verifier = ServerVerifier::builder()
3447            .dns_resolver(dns as Arc<dyn DnsResolver>)
3448            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3449            .with_dane_if_present()
3450            .build()
3451            .await
3452            .unwrap();
3453
3454        assert_eq!(verifier.dane_policy, DanePolicy::ValidateIfPresent);
3455    }
3456
3457    #[tokio::test]
3458    async fn test_builder_require_dane() {
3459        let dns = Arc::new(MockDnsResolver::new());
3460        let tlog = Arc::new(MockTransparencyLogClient::new());
3461
3462        let verifier = ServerVerifier::builder()
3463            .dns_resolver(dns as Arc<dyn DnsResolver>)
3464            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3465            .require_dane()
3466            .build()
3467            .await
3468            .unwrap();
3469
3470        assert_eq!(verifier.dane_policy, DanePolicy::Required);
3471    }
3472
3473    #[tokio::test]
3474    async fn test_builder_dane_port() {
3475        let dns = Arc::new(MockDnsResolver::new());
3476        let tlog = Arc::new(MockTransparencyLogClient::new());
3477
3478        let verifier = ServerVerifier::builder()
3479            .dns_resolver(dns as Arc<dyn DnsResolver>)
3480            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3481            .dane_port(8443)
3482            .build()
3483            .await
3484            .unwrap();
3485
3486        assert_eq!(verifier.dane_port, 8443);
3487    }
3488
3489    #[tokio::test]
3490    async fn test_builder_trusted_ra_domains() {
3491        let dns = Arc::new(MockDnsResolver::new());
3492        let tlog = Arc::new(MockTransparencyLogClient::new());
3493
3494        let verifier = ServerVerifier::builder()
3495            .dns_resolver(dns as Arc<dyn DnsResolver>)
3496            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
3497            .trusted_ra_domains(["tlog.example.com"])
3498            .build()
3499            .await
3500            .unwrap();
3501
3502        assert!(verifier.trusted_ra_domains.is_some());
3503        assert!(
3504            verifier
3505                .trusted_ra_domains
3506                .unwrap()
3507                .contains("tlog.example.com")
3508        );
3509    }
3510
3511    // =========================================================================
3512    // 7e: DANE Required failure path
3513    // =========================================================================
3514
3515    #[tokio::test]
3516    async fn test_dane_required_no_tlsa_records() {
3517        let host = "test.example.com";
3518        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3519
3520        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3521        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3522
3523        let dns_record = BadgeRecord {
3524            format_version: "ans-badge1".to_string(),
3525            version: Some(Version::new(1, 0, 0)),
3526            url: badge_url.to_string(),
3527        };
3528
3529        // No TLSA records configured — DANE Required should fail
3530        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3531        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3532
3533        let verifier = ServerVerifier {
3534            dns_resolver,
3535            tlog_client,
3536            cache: None,
3537            failure_policy: FailurePolicy::FailClosed,
3538            dane_policy: DanePolicy::Required,
3539            dane_port: 443,
3540            trusted_ra_domains: None,
3541        };
3542
3543        let cert = create_test_cert_identity(host, fingerprint);
3544        let fqdn = Fqdn::new(host).unwrap();
3545
3546        let outcome = verifier.verify(&fqdn, &cert).await;
3547        assert!(
3548            matches!(outcome, VerificationOutcome::DaneError(_)),
3549            "Expected DaneError for required DANE with no TLSA records, got: {outcome:?}"
3550        );
3551    }
3552
3553    // =========================================================================
3554    // VerificationOutcome helpers
3555    // =========================================================================
3556
3557    #[test]
3558    fn test_outcome_badge_returns_none_for_errors() {
3559        let outcome = VerificationOutcome::DnsError(DnsError::Timeout {
3560            fqdn: "test.example.com".to_string(),
3561        });
3562        assert!(outcome.badge().is_none());
3563
3564        let outcome = VerificationOutcome::NotAnsAgent {
3565            fqdn: "test.example.com".to_string(),
3566        };
3567        assert!(outcome.badge().is_none());
3568    }
3569
3570    #[test]
3571    fn test_outcome_badge_returns_some_for_mismatches() {
3572        let badge = create_test_badge(
3573            "test.example.com",
3574            "v1.0.0",
3575            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3576            "SHA256:aaa",
3577        );
3578
3579        let outcome = VerificationOutcome::HostnameMismatch {
3580            expected: "test.example.com".to_string(),
3581            actual: "other.example.com".to_string(),
3582            badge,
3583        };
3584        assert!(outcome.badge().is_some());
3585    }
3586
3587    #[test]
3588    fn test_server_verifier_debug_format() {
3589        let dbg = format!("{:?}", ServerVerifierBuilder::default());
3590        assert!(dbg.contains("ServerVerifierBuilder"));
3591    }
3592}