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 also public
34/// for programmatic use and testing.
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    /// Verification succeeded via SCITT receipt + status token (offline).
244    ///
245    /// This variant indicates the highest-assurance verification path: the
246    /// agent's identity was verified via cryptographic proofs without any
247    /// DNS or transparency log queries.
248    #[cfg(feature = "scitt")]
249    ScittVerified {
250        /// The verified status token payload.
251        status_token: crate::scitt::VerifiedStatusToken,
252        /// The verification tier achieved.
253        tier: ans_types::VerificationTier,
254        /// The fingerprint that matched.
255        matched_fingerprint: CertFingerprint,
256        /// The badge, if badge verification was also performed.
257        badge: Option<Badge>,
258    },
259
260    /// SCITT verification failed (cryptographic or structural error).
261    #[cfg(feature = "scitt")]
262    ScittError(crate::scitt::ScittError),
263}
264
265impl VerificationOutcome {
266    /// Check if verification was successful.
267    pub fn is_success(&self) -> bool {
268        match self {
269            Self::Verified { .. } => true,
270            #[cfg(feature = "scitt")]
271            Self::ScittVerified { .. } => true,
272            _ => false,
273        }
274    }
275
276    /// Check if the agent is in a terminal status (revoked, expired, etc.).
277    ///
278    /// Returns `true` for both badge-detected terminal status ([`InvalidStatus`])
279    /// and SCITT-detected terminal status ([`ScittError::TerminalStatus`] /
280    /// [`ScittError::AgentTerminal`]). Callers should use this instead of
281    /// pattern-matching individual variants.
282    pub fn is_terminal_status(&self) -> bool {
283        match self {
284            Self::InvalidStatus { status, .. } => status.should_reject(),
285            #[cfg(feature = "scitt")]
286            Self::ScittError(e) => e.is_terminal_status(),
287            _ => false,
288        }
289    }
290
291    /// Check if the agent is not registered with ANS.
292    pub fn is_not_ans_agent(&self) -> bool {
293        matches!(self, Self::NotAnsAgent { .. })
294    }
295
296    /// Get the badge if verification succeeded or partially completed.
297    pub fn badge(&self) -> Option<&Badge> {
298        match self {
299            Self::Verified { badge, .. }
300            | Self::InvalidStatus { badge, .. }
301            | Self::FingerprintMismatch { badge, .. }
302            | Self::HostnameMismatch { badge, .. }
303            | Self::AnsNameMismatch { badge, .. } => Some(badge),
304            #[cfg(feature = "scitt")]
305            Self::ScittVerified {
306                badge: Some(badge), ..
307            } => Some(badge),
308            _ => None,
309        }
310    }
311
312    /// Convert to a `Result`, returning the [`Badge`] on success.
313    ///
314    /// For badge-based verification, this is the natural accessor. For
315    /// SCITT-verified outcomes that may not carry a badge, prefer
316    /// [`into_scitt_result`](Self::into_scitt_result) which returns
317    /// `Option<Badge>` on success.
318    ///
319    /// When the `scitt` feature is enabled:
320    /// - `ScittVerified` with a badge → `Ok(badge)`
321    /// - `ScittVerified` without a badge → `Err(Configuration)` (use
322    ///   `into_scitt_result()` instead)
323    /// - `ScittError` → `Err(Scitt(..))`
324    pub fn into_result(self) -> AnsResult<Badge> {
325        match self {
326            Self::Verified { badge, .. } => Ok(badge),
327            Self::NotAnsAgent { fqdn } => Err(AnsError::Dns(DnsError::NotFound { fqdn })),
328            Self::InvalidStatus { status, .. } => {
329                Err(AnsError::Verification(VerificationError::InvalidStatus {
330                    status,
331                }))
332            }
333            Self::FingerprintMismatch {
334                expected, actual, ..
335            } => Err(AnsError::Verification(
336                VerificationError::FingerprintMismatch { expected, actual },
337            )),
338            Self::HostnameMismatch {
339                expected, actual, ..
340            } => Err(AnsError::Verification(
341                VerificationError::HostnameMismatch { expected, actual },
342            )),
343            Self::AnsNameMismatch {
344                expected, actual, ..
345            } => Err(AnsError::Verification(VerificationError::AnsNameMismatch {
346                expected,
347                actual,
348            })),
349            Self::DnsError(e) => Err(AnsError::Dns(e)),
350            Self::TlogError(e) => Err(AnsError::TransparencyLog(e)),
351            Self::CertError(e) => Err(AnsError::Certificate(e)),
352            Self::ParseError(e) => Err(AnsError::Parse(e)),
353            Self::DaneError(e) => Err(AnsError::Verification(
354                VerificationError::DaneVerificationFailed(e),
355            )),
356            #[cfg(feature = "scitt")]
357            Self::ScittVerified { badge: Some(b), .. } => Ok(b),
358            #[cfg(feature = "scitt")]
359            Self::ScittVerified { badge: None, .. } => Err(AnsError::Verification(
360                VerificationError::Configuration(
361                    "SCITT verification succeeded without badge; use into_scitt_result() for SCITT-aware callers".to_string(),
362                ),
363            )),
364            #[cfg(feature = "scitt")]
365            Self::ScittError(e) => Err(AnsError::Scitt(e)),
366        }
367    }
368
369    /// Convert a SCITT-aware outcome to a `Result`.
370    ///
371    /// Returns `Ok(Some(badge))` for badge or SCITT+badge verification,
372    /// `Ok(None)` for pure SCITT verification without a badge, and
373    /// `Err(..)` for any failure.
374    ///
375    /// This method is consistent with [`is_success()`](Self::is_success):
376    /// every outcome where `is_success()` returns `true` maps to `Ok(..)`.
377    #[cfg(feature = "scitt")]
378    pub fn into_scitt_result(self) -> AnsResult<Option<Badge>> {
379        match self {
380            Self::Verified { badge, .. } => Ok(Some(badge)),
381            Self::ScittVerified { badge, .. } => Ok(badge),
382            other => other.into_result().map(Some),
383        }
384    }
385}
386
387/// SCITT verification tier policy.
388///
389/// Controls how SCITT and badge verification interact.
390#[cfg(feature = "scitt")]
391#[derive(Debug, Clone, Copy, Default)]
392#[non_exhaustive]
393pub enum ScittTierPolicy {
394    /// Try SCITT first, fall back to badge if headers absent (recommended).
395    ///
396    /// Present headers are final: any SCITT failure (including `TokenExpired`)
397    /// is a hard reject with no badge fallback. Badge fallback only occurs
398    /// when SCITT headers are completely absent.
399    #[default]
400    ScittWithBadgeFallback,
401
402    /// SCITT required; fail if SCITT verification doesn't succeed.
403    ///
404    /// Only safe when 100% of peers support SCITT. `TokenExpired` is a
405    /// hard failure under this policy (no badge fallback available).
406    RequireScitt,
407
408    /// Badge first, enhance with SCITT if headers present.
409    ///
410    /// For gradual migration: existing badge verification runs first,
411    /// SCITT supplements the result if headers are available.
412    BadgeWithScittEnhancement,
413}
414
415/// Configuration for SCITT verification.
416#[cfg(feature = "scitt")]
417#[derive(Debug, Clone)]
418#[non_exhaustive]
419pub struct ScittConfig {
420    /// How SCITT and badge verification interact.
421    pub tier_policy: ScittTierPolicy,
422    /// Clock skew tolerance for status token expiry checks.
423    pub clock_skew_tolerance: Duration,
424}
425
426#[cfg(feature = "scitt")]
427impl Default for ScittConfig {
428    fn default() -> Self {
429        Self {
430            tier_policy: ScittTierPolicy::default(),
431            clock_skew_tolerance: Duration::from_secs(60),
432        }
433    }
434}
435
436#[cfg(feature = "scitt")]
437impl ScittConfig {
438    /// Create with default settings.
439    pub fn new() -> Self {
440        Self::default()
441    }
442
443    /// Set the tier policy.
444    pub fn with_tier_policy(mut self, policy: ScittTierPolicy) -> Self {
445        self.tier_policy = policy;
446        self
447    }
448
449    /// Set the clock skew tolerance.
450    pub fn with_clock_skew(mut self, tolerance: Duration) -> Self {
451        self.clock_skew_tolerance = tolerance;
452        self
453    }
454}
455
456/// Failure handling policy.
457#[derive(Debug, Clone, Copy, Default)]
458#[non_exhaustive]
459pub enum FailurePolicy {
460    /// Reject on any failure (most secure).
461    #[default]
462    FailClosed,
463
464    /// Use cached badge if available, otherwise reject.
465    FailOpenWithCache {
466        /// Maximum age of cached badge to accept.
467        max_staleness: Duration,
468    },
469}
470
471/// Validate that a badge URL's domain is in the trusted RA domains set.
472///
473/// Returns `Ok(())` if:
474/// - `trusted` is `None` (no restriction configured — allow all domains)
475/// - The URL's host is present in the trusted set
476///
477/// Returns `Err(TlogError::UntrustedDomain)` if the host is not trusted.
478fn validate_badge_domain(trusted: Option<&HashSet<String>>, url: &str) -> Result<(), TlogError> {
479    let Some(trusted) = trusted else {
480        return Ok(());
481    };
482    let parsed = url::Url::parse(url)
483        .map_err(|e| TlogError::InvalidUrl(format!("Badge URL is invalid: {e}")))?;
484    let domain = parsed
485        .host_str()
486        .ok_or_else(|| TlogError::InvalidUrl(format!("Badge URL has no host: {url}")))?;
487    if trusted.contains(domain) {
488        Ok(())
489    } else {
490        Err(TlogError::UntrustedDomain {
491            domain: domain.to_string(),
492            trusted: trusted.iter().cloned().collect(),
493        })
494    }
495}
496
497/// Server verifier for clients verifying agent servers.
498pub struct ServerVerifier {
499    dns_resolver: Arc<dyn DnsResolver>,
500    tlog_client: Arc<dyn TransparencyLogClient>,
501    cache: Option<Arc<BadgeCache>>,
502    failure_policy: FailurePolicy,
503    dane_policy: DanePolicy,
504    /// Port to use for TLSA lookups (default: 443).
505    dane_port: u16,
506    /// Optional set of trusted RA domains for badge URL validation.
507    trusted_ra_domains: Option<HashSet<String>>,
508}
509
510impl fmt::Debug for ServerVerifier {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        f.debug_struct("ServerVerifier")
513            .field("failure_policy", &self.failure_policy)
514            .field("dane_policy", &self.dane_policy)
515            .field("dane_port", &self.dane_port)
516            .field("has_cache", &self.cache.is_some())
517            .field("has_trusted_ra_domains", &self.trusted_ra_domains.is_some())
518            .finish_non_exhaustive()
519    }
520}
521
522impl ServerVerifier {
523    /// Create a new builder.
524    pub fn builder() -> ServerVerifierBuilder {
525        ServerVerifierBuilder::default()
526    }
527
528    /// Verify an agent server.
529    ///
530    /// # Steps
531    /// 1. DNS lookup for `_ans-badge` TXT record (with `_ra-badge` fallback)
532    /// 2. Fetch preferred badge from transparency log (newest ACTIVE first)
533    /// 3. Validate badge status
534    /// 4. Compare certificate fingerprint to badge
535    /// 5. On mismatch with multiple records, try all badges by fingerprint
536    ///    (handles multi-version transitions where both versions are ACTIVE)
537    /// 6. If still no match, refresh-on-mismatch (handles cert renewal)
538    /// 7. Compare certificate CN to badge agent.host
539    pub async fn verify(&self, fqdn: &Fqdn, server_cert: &CertIdentity) -> VerificationOutcome {
540        tracing::info!(fqdn = %fqdn, "Starting server verification");
541        tracing::debug!(
542            cert_cn = ?server_cert.common_name,
543            cert_fingerprint = %server_cert.fingerprint,
544            "Certificate details"
545        );
546
547        // Check cache first — scan all versioned badges by fingerprint
548        if let Some(cache) = &self.cache {
549            let cached_badges = cache.get_all_for_fqdn(fqdn).await;
550            if !cached_badges.is_empty() {
551                tracing::debug!(fqdn = %fqdn, count = cached_badges.len(), "Scanning cached badges");
552                for cached in &cached_badges {
553                    let outcome = self.verify_against_badge(&cached.badge, server_cert, true);
554                    if outcome.is_success() {
555                        tracing::debug!(fqdn = %fqdn, "Cache hit — badge matched");
556                        return outcome;
557                    }
558                }
559                // No cached badge matched — fall through to DNS+TLog
560                tracing::info!(fqdn = %fqdn, "No cached badge matched fingerprint, fetching fresh");
561            }
562        }
563
564        // DNS lookup
565        tracing::debug!(fqdn = %fqdn, "Performing DNS lookup for _ans-badge / _ra-badge");
566        let records = match self.dns_resolver.lookup_badge(fqdn).await {
567            Ok(DnsLookupResult::Found(records)) => {
568                tracing::debug!(count = records.len(), "Found badge records");
569                for (i, r) in records.iter().enumerate() {
570                    tracing::debug!(index = i, version = ?r.version, url = %r.url, "Badge record");
571                }
572                records
573            }
574            Ok(DnsLookupResult::NotFound) => {
575                tracing::warn!(fqdn = %fqdn, "No badge record found - not an ANS agent");
576                return VerificationOutcome::NotAnsAgent {
577                    fqdn: fqdn.to_string(),
578                };
579            }
580            Err(e) => {
581                tracing::error!(fqdn = %fqdn, error = %e, "DNS lookup failed");
582                return self.handle_dns_error(e, fqdn, server_cert).await;
583            }
584        };
585
586        // Server certs don't contain version info. Try all badge records by
587        // fingerprint to handle multi-version transitions where both versions
588        // are ACTIVE (see AGENT_TO_AGENT_FLOW §5.3).
589        let outcome = self
590            .verify_against_records(&records, fqdn, server_cert)
591            .await;
592
593        if !outcome.is_success() {
594            return outcome;
595        }
596
597        // Perform DANE verification if enabled
598        if self.dane_policy.should_verify() {
599            match self.verify_dane(fqdn, server_cert).await {
600                Ok(result) => {
601                    if !result.is_acceptable(self.dane_policy) {
602                        tracing::error!(
603                            fqdn = %fqdn,
604                            dane_policy = ?self.dane_policy,
605                            "DANE verification failed"
606                        );
607                        return VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
608                    }
609                }
610                Err(e) => {
611                    tracing::error!(fqdn = %fqdn, error = %e, "DANE verification error");
612                    return VerificationOutcome::DaneError(e);
613                }
614            }
615        }
616
617        outcome
618    }
619
620    /// Try all badge records to find one matching the server certificate.
621    ///
622    /// Server certificates don't contain version info, so during multi-version
623    /// transitions (where multiple versions are ACTIVE), we must try each badge
624    /// by fingerprint comparison. Prefers newest ACTIVE badge, falls back to
625    /// older versions and AHP-deprecated badges.
626    ///
627    /// If no badge matches from any record, falls back to refresh-on-mismatch
628    /// (handles the certificate renewal case where the `TLog` was recently updated).
629    async fn verify_against_records(
630        &self,
631        records: &[BadgeRecord],
632        fqdn: &Fqdn,
633        server_cert: &CertIdentity,
634    ) -> VerificationOutcome {
635        // Sort by version descending (newest first)
636        let mut sorted: Vec<_> = records.iter().collect();
637        sorted.sort_by(|a, b| b.version.cmp(&a.version));
638
639        // Pre-populate the version index from DNS records
640        if let Some(cache) = &self.cache {
641            let versions: Vec<Version> =
642                sorted.iter().filter_map(|r| r.version().cloned()).collect();
643            if !versions.is_empty() {
644                cache.set_version_index(fqdn, versions).await;
645            }
646        }
647
648        // Fetch all badges in parallel
649        let results = self.fetch_badges_parallel(&sorted).await;
650
651        let mut last_mismatch: Option<VerificationOutcome> = None;
652        let mut last_error: Option<AnsError> = None;
653
654        for (record, result) in results {
655            let badge = match result {
656                Ok(b) => b,
657                Err(e) => {
658                    tracing::debug!(url = %record.url, error = %e, "Failed to fetch badge, trying next");
659                    last_error = Some(AnsError::TransparencyLog(e));
660                    continue;
661                }
662            };
663
664            tracing::debug!(
665                version = ?record.version,
666                status = ?badge.status,
667                "Checking badge record"
668            );
669
670            // Cache every successfully fetched badge by version
671            if let Some(cache) = &self.cache {
672                let version = record
673                    .version()
674                    .cloned()
675                    .or_else(|| badge.agent_version().parse::<Version>().ok());
676                if let Some(v) = &version {
677                    cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
678                    tracing::debug!(fqdn = %fqdn, version = %v, "Cached badge by version");
679                }
680            }
681
682            let outcome = self.verify_against_badge(&badge, server_cert, true);
683
684            match &outcome {
685                VerificationOutcome::Verified { .. } => {
686                    return outcome;
687                }
688                VerificationOutcome::FingerprintMismatch { .. } => {
689                    tracing::debug!(version = ?record.version, "Fingerprint mismatch, trying next record");
690                    last_mismatch = Some(outcome);
691                }
692                // Non-fingerprint failures (status rejected, hostname mismatch) are terminal
693                _ => return outcome,
694            }
695        }
696
697        // No badge matched by fingerprint. Try refresh-on-mismatch for the
698        // cert renewal case (TLog updated after our DNS lookup).
699        if last_mismatch.is_some() {
700            tracing::info!(fqdn = %fqdn, "No badge matched, attempting refresh-on-mismatch");
701            return self.verify_with_refresh(fqdn, server_cert).await;
702        }
703
704        // All fetches failed
705        match last_error {
706            Some(e) => self.handle_ans_error(e, fqdn, server_cert).await,
707            None => VerificationOutcome::NotAnsAgent {
708                fqdn: fqdn.to_string(),
709            },
710        }
711    }
712
713    /// Perform DANE/TLSA verification.
714    async fn verify_dane(
715        &self,
716        fqdn: &Fqdn,
717        cert: &CertIdentity,
718    ) -> Result<DaneVerificationResult, DaneError> {
719        tracing::debug!(
720            fqdn = %fqdn,
721            port = self.dane_port,
722            policy = ?self.dane_policy,
723            "Starting DANE verification"
724        );
725
726        let tlsa_records = self
727            .dns_resolver
728            .get_tlsa_records(fqdn, self.dane_port)
729            .await?;
730
731        verify_dane(
732            &tlsa_records,
733            &cert.fingerprint,
734            self.dane_policy,
735            fqdn,
736            self.dane_port,
737        )
738    }
739
740    /// Pre-fetch badges for caching (before TLS connection).
741    ///
742    /// Fetches ALL badge records from DNS, then fetches and caches each badge
743    /// by version. Returns the preferred (newest active) badge.
744    pub async fn prefetch(&self, fqdn: &Fqdn) -> Result<Badge, AnsError> {
745        let records = match self.dns_resolver.lookup_badge(fqdn).await {
746            Ok(DnsLookupResult::Found(records)) => records,
747            Ok(DnsLookupResult::NotFound) => {
748                return Err(AnsError::Dns(DnsError::NotFound {
749                    fqdn: fqdn.to_string(),
750                }));
751            }
752            Err(e) => return Err(AnsError::Dns(e)),
753        };
754
755        // Sort by version descending (newest first)
756        let mut sorted: Vec<_> = records.iter().collect();
757        sorted.sort_by(|a, b| b.version.cmp(&a.version));
758
759        // Pre-populate the version index from DNS records
760        if let Some(cache) = &self.cache {
761            let versions: Vec<Version> =
762                sorted.iter().filter_map(|r| r.version().cloned()).collect();
763            if !versions.is_empty() {
764                cache.set_version_index(fqdn, versions).await;
765            }
766        }
767
768        // Fetch ALL badges in parallel, then process results
769        let results = self.fetch_badges_parallel(&sorted).await;
770
771        let mut preferred: Option<Badge> = None;
772        let mut last_error = None;
773
774        for (record, result) in results {
775            match result {
776                Ok(badge) => {
777                    // Cache by version
778                    if let Some(cache) = &self.cache {
779                        let version = record
780                            .version()
781                            .cloned()
782                            .or_else(|| badge.agent_version().parse::<Version>().ok());
783                        if let Some(v) = &version {
784                            cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
785                            tracing::debug!(fqdn = %fqdn, version = %v, "Prefetch: cached badge");
786                        }
787                    }
788
789                    // Track preferred (first active, then deprecated, as fallback)
790                    if preferred.is_none()
791                        && (badge.status.is_active() || badge.status == BadgeStatus::Deprecated)
792                    {
793                        preferred = Some(badge);
794                    }
795                }
796                Err(e) => {
797                    last_error = Some(e);
798                }
799            }
800        }
801
802        match preferred {
803            Some(badge) => Ok(badge),
804            None => match last_error {
805                Some(e) => Err(AnsError::TransparencyLog(e)),
806                None => Err(AnsError::TransparencyLog(TlogError::InvalidResponse(
807                    "no badge records available".to_string(),
808                ))),
809            },
810        }
811    }
812
813    /// Refresh-on-mismatch: invalidate cache, re-fetch from DNS and `TLog`, re-verify.
814    ///
815    /// Called when no badge record matched by fingerprint. Handles the cert
816    /// renewal case where the `TLog` was updated after the initial fetch.
817    /// Tries all records to also handle multi-version transitions.
818    async fn verify_with_refresh(
819        &self,
820        fqdn: &Fqdn,
821        server_cert: &CertIdentity,
822    ) -> VerificationOutcome {
823        // Invalidate all cached badges for this FQDN
824        if let Some(cache) = &self.cache {
825            cache.invalidate_fqdn(fqdn).await;
826        }
827
828        // Re-fetch from DNS + tlog, trying all records
829        let records = match self.dns_resolver.lookup_badge(fqdn).await {
830            Ok(DnsLookupResult::Found(records)) => records,
831            Ok(DnsLookupResult::NotFound) => {
832                return VerificationOutcome::NotAnsAgent {
833                    fqdn: fqdn.to_string(),
834                };
835            }
836            Err(e) => return VerificationOutcome::DnsError(e),
837        };
838
839        // Try all records — this is the final answer (no further refresh)
840        self.verify_against_records_final(&records, fqdn, server_cert)
841            .await
842    }
843
844    /// Try all badge records without further refresh fallback (terminal attempt).
845    async fn verify_against_records_final(
846        &self,
847        records: &[BadgeRecord],
848        fqdn: &Fqdn,
849        server_cert: &CertIdentity,
850    ) -> VerificationOutcome {
851        let mut sorted: Vec<_> = records.iter().collect();
852        sorted.sort_by(|a, b| b.version.cmp(&a.version));
853
854        // Pre-populate the version index from DNS records
855        if let Some(cache) = &self.cache {
856            let versions: Vec<Version> =
857                sorted.iter().filter_map(|r| r.version().cloned()).collect();
858            if !versions.is_empty() {
859                cache.set_version_index(fqdn, versions).await;
860            }
861        }
862
863        // Fetch all badges in parallel
864        let results = self.fetch_badges_parallel(&sorted).await;
865
866        let mut last_mismatch: Option<VerificationOutcome> = None;
867        let mut last_error: Option<AnsError> = None;
868
869        for (record, result) in results {
870            let badge = match result {
871                Ok(b) => b,
872                Err(e) => {
873                    last_error = Some(AnsError::TransparencyLog(e));
874                    continue;
875                }
876            };
877
878            // Cache every successfully fetched badge by version
879            if let Some(cache) = &self.cache {
880                let version = record
881                    .version()
882                    .cloned()
883                    .or_else(|| badge.agent_version().parse::<Version>().ok());
884                if let Some(v) = &version {
885                    cache.insert_for_fqdn_version(fqdn, v, badge.clone()).await;
886                }
887            }
888
889            let outcome = self.verify_against_badge(&badge, server_cert, true);
890
891            match &outcome {
892                VerificationOutcome::Verified { .. } => {
893                    return outcome;
894                }
895                VerificationOutcome::FingerprintMismatch { .. } => {
896                    last_mismatch = Some(outcome);
897                }
898                _ => return outcome,
899            }
900        }
901
902        // Return last mismatch or last error
903        if let Some(mismatch) = last_mismatch {
904            return mismatch;
905        }
906        match last_error {
907            Some(e) => self.handle_ans_error(e, fqdn, server_cert).await,
908            None => VerificationOutcome::NotAnsAgent {
909                fqdn: fqdn.to_string(),
910            },
911        }
912    }
913
914    /// Fetch badges from the transparency log in parallel.
915    ///
916    /// Validates badge domains first (pure check), then fires all HTTP requests
917    /// concurrently via `join_all`. Returns results paired with their records,
918    /// preserving the input ordering.
919    async fn fetch_badges_parallel<'a>(
920        &self,
921        records: &'a [&'a BadgeRecord],
922    ) -> Vec<(&'a BadgeRecord, Result<Badge, TlogError>)> {
923        // Pair each record with a future: domain-invalid records get an
924        // immediate Err, valid ones get a real TLog fetch.
925        let futures: Vec<_> = records
926            .iter()
927            .map(|record| {
928                let tlog = &self.tlog_client;
929                let trusted = &self.trusted_ra_domains;
930                async move {
931                    if let Err(e) = validate_badge_domain(trusted.as_ref(), &record.url) {
932                        (*record, Err(e))
933                    } else {
934                        let result = tlog.fetch_badge(&record.url).await;
935                        (*record, result)
936                    }
937                }
938            })
939            .collect();
940
941        join_all(futures).await
942    }
943
944    #[allow(clippy::unused_self)] // logically part of ServerVerifier; may use self in future
945    fn verify_against_badge(
946        &self,
947        badge: &Badge,
948        cert: &CertIdentity,
949        is_server: bool,
950    ) -> VerificationOutcome {
951        let cert_type = if is_server { "server" } else { "identity" };
952        tracing::debug!(cert_type, "Verifying certificate against badge");
953
954        // Check status
955        if badge.status.should_reject() {
956            tracing::warn!(
957                status = ?badge.status,
958                "Badge status is not valid for connections"
959            );
960            return VerificationOutcome::InvalidStatus {
961                status: badge.status,
962                badge: badge.clone(),
963            };
964        }
965        tracing::debug!(status = ?badge.status, "Badge status is valid");
966
967        // Compare fingerprint
968        let expected_fp = if is_server {
969            badge.server_cert_fingerprint()
970        } else {
971            badge.identity_cert_fingerprint()
972        };
973
974        tracing::debug!(
975            expected = %expected_fp,
976            actual = %cert.fingerprint,
977            "Comparing certificate fingerprints"
978        );
979
980        if !cert.fingerprint.matches(expected_fp) {
981            tracing::error!(
982                expected = %expected_fp,
983                actual = %cert.fingerprint,
984                "Certificate fingerprint MISMATCH"
985            );
986            return VerificationOutcome::FingerprintMismatch {
987                expected: expected_fp.to_string(),
988                actual: cert.fingerprint.to_string(),
989                badge: badge.clone(),
990            };
991        }
992        tracing::debug!("Fingerprint matches");
993
994        // Compare hostname
995        let expected_host = badge.agent_host();
996        let actual_host = cert.fqdn().unwrap_or("");
997
998        tracing::debug!(
999            expected = %expected_host,
1000            actual = %actual_host,
1001            "Comparing hostnames"
1002        );
1003
1004        if !actual_host.eq_ignore_ascii_case(expected_host) {
1005            tracing::error!(
1006                expected = %expected_host,
1007                actual = %actual_host,
1008                "Hostname MISMATCH"
1009            );
1010            return VerificationOutcome::HostnameMismatch {
1011                expected: expected_host.to_string(),
1012                actual: actual_host.to_string(),
1013                badge: badge.clone(),
1014            };
1015        }
1016
1017        tracing::info!(
1018            agent = %badge.agent_name(),
1019            host = %badge.agent_host(),
1020            "Verification SUCCESSFUL"
1021        );
1022        VerificationOutcome::Verified {
1023            badge: badge.clone(),
1024            matched_fingerprint: cert.fingerprint.clone(),
1025        }
1026    }
1027
1028    async fn handle_dns_error(
1029        &self,
1030        error: DnsError,
1031        fqdn: &Fqdn,
1032        cert: &CertIdentity,
1033    ) -> VerificationOutcome {
1034        match self.failure_policy {
1035            FailurePolicy::FailClosed => VerificationOutcome::DnsError(error),
1036            FailurePolicy::FailOpenWithCache { max_staleness } => {
1037                if let Some(cache) = &self.cache {
1038                    for cached in cache.get_all_for_fqdn(fqdn).await {
1039                        if cached.fetched_at.elapsed() < max_staleness {
1040                            let outcome = self.verify_against_badge(&cached.badge, cert, true);
1041                            if outcome.is_success() {
1042                                return outcome;
1043                            }
1044                        }
1045                    }
1046                }
1047                VerificationOutcome::DnsError(error)
1048            }
1049        }
1050    }
1051
1052    async fn handle_ans_error(
1053        &self,
1054        error: AnsError,
1055        fqdn: &Fqdn,
1056        cert: &CertIdentity,
1057    ) -> VerificationOutcome {
1058        match self.failure_policy {
1059            FailurePolicy::FailClosed => match error {
1060                AnsError::TransparencyLog(e) => VerificationOutcome::TlogError(e),
1061                AnsError::Dns(e) => VerificationOutcome::DnsError(e),
1062                AnsError::Certificate(e) => VerificationOutcome::CertError(e),
1063                AnsError::Parse(e) => VerificationOutcome::ParseError(e),
1064                AnsError::Verification(_) => VerificationOutcome::NotAnsAgent {
1065                    fqdn: fqdn.to_string(),
1066                },
1067                // SCITT errors should not reach badge-path error handling;
1068                // if they do, log loudly and treat as a generic verification failure.
1069                #[cfg(feature = "scitt")]
1070                AnsError::Scitt(ref e) => {
1071                    tracing::error!(
1072                        error = %e,
1073                        fqdn = %fqdn,
1074                        "BUG: ScittError reached badge-path error handler — treating as NotAnsAgent"
1075                    );
1076                    VerificationOutcome::NotAnsAgent {
1077                        fqdn: fqdn.to_string(),
1078                    }
1079                }
1080            },
1081            FailurePolicy::FailOpenWithCache { max_staleness } => {
1082                if let Some(cache) = &self.cache {
1083                    for cached in cache.get_all_for_fqdn(fqdn).await {
1084                        if cached.fetched_at.elapsed() < max_staleness {
1085                            let outcome = self.verify_against_badge(&cached.badge, cert, true);
1086                            if outcome.is_success() {
1087                                return outcome;
1088                            }
1089                        }
1090                    }
1091                }
1092                match error {
1093                    AnsError::TransparencyLog(e) => VerificationOutcome::TlogError(e),
1094                    AnsError::Dns(e) => VerificationOutcome::DnsError(e),
1095                    AnsError::Certificate(e) => VerificationOutcome::CertError(e),
1096                    AnsError::Parse(e) => VerificationOutcome::ParseError(e),
1097                    AnsError::Verification(_) => VerificationOutcome::NotAnsAgent {
1098                        fqdn: fqdn.to_string(),
1099                    },
1100                    // SCITT errors should not reach badge-path error handling;
1101                    // if they do, log loudly and treat as a generic verification failure.
1102                    #[cfg(feature = "scitt")]
1103                    AnsError::Scitt(ref e) => {
1104                        tracing::error!(
1105                            error = %e,
1106                            fqdn = %fqdn,
1107                            "BUG: ScittError reached badge-path error handler — treating as NotAnsAgent"
1108                        );
1109                        VerificationOutcome::NotAnsAgent {
1110                            fqdn: fqdn.to_string(),
1111                        }
1112                    }
1113                }
1114            }
1115        }
1116    }
1117}
1118
1119/// Builder for `ServerVerifier`.
1120#[derive(Default)]
1121pub struct ServerVerifierBuilder {
1122    dns_resolver: Option<Arc<dyn DnsResolver>>,
1123    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
1124    cache: Option<Arc<BadgeCache>>,
1125    failure_policy: FailurePolicy,
1126    dane_policy: DanePolicy,
1127    dane_port: Option<u16>,
1128    trusted_ra_domains: Option<HashSet<String>>,
1129}
1130
1131impl fmt::Debug for ServerVerifierBuilder {
1132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1133        f.debug_struct("ServerVerifierBuilder")
1134            .field("failure_policy", &self.failure_policy)
1135            .field("dane_policy", &self.dane_policy)
1136            .field("dane_port", &self.dane_port)
1137            .field("has_dns_resolver", &self.dns_resolver.is_some())
1138            .field("has_tlog_client", &self.tlog_client.is_some())
1139            .field("has_cache", &self.cache.is_some())
1140            .finish_non_exhaustive()
1141    }
1142}
1143
1144impl ServerVerifierBuilder {
1145    /// Set a custom DNS resolver.
1146    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
1147        self.dns_resolver = Some(resolver);
1148        self
1149    }
1150
1151    /// Set a custom transparency log client.
1152    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
1153        self.tlog_client = Some(client);
1154        self
1155    }
1156
1157    /// Enable caching with default configuration.
1158    pub fn with_cache(mut self) -> Self {
1159        self.cache = Some(Arc::new(BadgeCache::with_defaults()));
1160        self
1161    }
1162
1163    /// Enable caching with custom configuration.
1164    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
1165        self.cache = Some(Arc::new(BadgeCache::new(config)));
1166        self
1167    }
1168
1169    /// Use an existing cache.
1170    pub fn cache(mut self, cache: Arc<BadgeCache>) -> Self {
1171        self.cache = Some(cache);
1172        self
1173    }
1174
1175    /// Set the failure policy.
1176    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
1177        self.failure_policy = policy;
1178        self
1179    }
1180
1181    /// Set the DANE/TLSA verification policy.
1182    ///
1183    /// - `DanePolicy::Disabled`: Skip DANE verification entirely (default)
1184    /// - `DanePolicy::ValidateIfPresent`: Verify TLSA if records exist, skip if not
1185    /// - `DanePolicy::Required`: Require TLSA records to exist and match
1186    pub fn dane_policy(mut self, policy: DanePolicy) -> Self {
1187        self.dane_policy = policy;
1188        self
1189    }
1190
1191    /// Enable DANE verification if TLSA records are present.
1192    ///
1193    /// This is a convenience method equivalent to `.dane_policy(DanePolicy::ValidateIfPresent)`.
1194    pub fn with_dane_if_present(mut self) -> Self {
1195        self.dane_policy = DanePolicy::ValidateIfPresent;
1196        self
1197    }
1198
1199    /// Require DANE verification (fail if no TLSA records).
1200    ///
1201    /// This is a convenience method equivalent to `.dane_policy(DanePolicy::Required)`.
1202    pub fn require_dane(mut self) -> Self {
1203        self.dane_policy = DanePolicy::Required;
1204        self
1205    }
1206
1207    /// Set the port for TLSA lookups (default: 443).
1208    pub fn dane_port(mut self, port: u16) -> Self {
1209        self.dane_port = Some(port);
1210        self
1211    }
1212
1213    /// Restrict badge URL fetches to a set of trusted RA domains.
1214    ///
1215    /// When configured, badge URLs discovered via DNS TXT records will be
1216    /// validated against this set before any HTTP request is made. URLs
1217    /// pointing to hosts not in the set are rejected with
1218    /// `TlogError::UntrustedDomain`.
1219    ///
1220    /// By default (`None`), all domains are allowed.
1221    pub fn trusted_ra_domains(
1222        mut self,
1223        domains: impl IntoIterator<Item = impl Into<String>>,
1224    ) -> Self {
1225        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
1226        self
1227    }
1228
1229    /// Build the verifier.
1230    pub async fn build(self) -> AnsResult<ServerVerifier> {
1231        let dns_resolver = match self.dns_resolver {
1232            Some(r) => r,
1233            None => Arc::new(
1234                HickoryDnsResolver::new()
1235                    .await
1236                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1237            ),
1238        };
1239
1240        let tlog_client = self
1241            .tlog_client
1242            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
1243
1244        Ok(ServerVerifier {
1245            dns_resolver,
1246            tlog_client,
1247            cache: self.cache,
1248            failure_policy: self.failure_policy,
1249            dane_policy: self.dane_policy,
1250            dane_port: self.dane_port.unwrap_or(443),
1251            trusted_ra_domains: self.trusted_ra_domains,
1252        })
1253    }
1254}
1255
1256/// Client verifier for servers verifying mTLS agent clients.
1257pub struct ClientVerifier {
1258    dns_resolver: Arc<dyn DnsResolver>,
1259    tlog_client: Arc<dyn TransparencyLogClient>,
1260    cache: Option<Arc<BadgeCache>>,
1261    failure_policy: FailurePolicy,
1262    /// Optional set of trusted RA domains for badge URL validation.
1263    trusted_ra_domains: Option<HashSet<String>>,
1264}
1265
1266impl fmt::Debug for ClientVerifier {
1267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1268        f.debug_struct("ClientVerifier")
1269            .field("failure_policy", &self.failure_policy)
1270            .field("has_cache", &self.cache.is_some())
1271            .field("has_trusted_ra_domains", &self.trusted_ra_domains.is_some())
1272            .finish_non_exhaustive()
1273    }
1274}
1275
1276impl ClientVerifier {
1277    /// Create a new builder.
1278    pub fn builder() -> ClientVerifierBuilder {
1279        ClientVerifierBuilder::default()
1280    }
1281
1282    /// Verify an mTLS client certificate.
1283    ///
1284    /// # Steps
1285    /// 1. Extract CN (FQDN) and URI SAN (`ANSName`) from certificate
1286    /// 2. Parse version from `ANSName`
1287    /// 3. DNS lookup for `_ans-badge` (with `_ra-badge` fallback) using CN as FQDN
1288    /// 4. Select badge matching version from certificate
1289    /// 5. Compare identity cert fingerprint, CN, and URI SAN to badge
1290    /// 6. On fingerprint mismatch, refresh badge and re-verify once
1291    #[allow(clippy::too_many_lines)] // verification flow reads best as a single method
1292    pub async fn verify(&self, client_cert: &CertIdentity) -> VerificationOutcome {
1293        tracing::info!("Starting mTLS client verification");
1294        tracing::debug!(
1295            cn = ?client_cert.common_name,
1296            dns_sans = ?client_cert.dns_sans,
1297            uri_sans = ?client_cert.uri_sans,
1298            fingerprint = %client_cert.fingerprint,
1299            "Client certificate details"
1300        );
1301
1302        // Extract FQDN from certificate
1303        let Some(fqdn_str) = client_cert.fqdn() else {
1304            tracing::error!("No CN or DNS SAN found in client certificate");
1305            return VerificationOutcome::CertError(CryptoError::NoCommonName);
1306        };
1307
1308        let fqdn = match Fqdn::new(fqdn_str) {
1309            Ok(f) => f,
1310            Err(e) => {
1311                tracing::error!(fqdn = %fqdn_str, error = %e, "Invalid FQDN in certificate");
1312                return VerificationOutcome::ParseError(e);
1313            }
1314        };
1315        tracing::debug!(fqdn = %fqdn, "Extracted FQDN from certificate");
1316
1317        // Extract ANS name and version from URI SAN
1318        let ans_name = if let Some(n) = client_cert.ans_name() {
1319            tracing::debug!(ans_name = %n, "Found ANS name in URI SAN");
1320            n
1321        } else {
1322            tracing::error!(uri_sans = ?client_cert.uri_sans, "No ANS name (ans://) found in URI SANs");
1323            return VerificationOutcome::CertError(CryptoError::NoUriSan);
1324        };
1325
1326        let version = ans_name.version().clone();
1327        tracing::debug!(version = %version, "Parsed version from ANS name");
1328
1329        // Check cache first
1330        if let Some(cache) = &self.cache
1331            && let Some(cached) = cache.get_by_fqdn_version(&fqdn, &version).await
1332        {
1333            tracing::debug!(fqdn = %fqdn, version = %version, "Using cached badge");
1334            let outcome = self.verify_client_against_badge(&cached.badge, client_cert, &ans_name);
1335            // Refresh-on-mismatch for cached badges
1336            if matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }) {
1337                tracing::info!(fqdn = %fqdn, "Fingerprint mismatch on cached badge, refreshing");
1338                return self
1339                    .verify_client_with_refresh(&fqdn, &version, client_cert, &ans_name)
1340                    .await;
1341            }
1342            return outcome;
1343        }
1344
1345        // DNS lookup
1346        tracing::debug!(fqdn = %fqdn, version = %version, "Looking up badge for version");
1347        let badge_record = match self
1348            .dns_resolver
1349            .find_badge_for_version(&fqdn, &version)
1350            .await
1351        {
1352            Ok(Some(record)) => {
1353                tracing::debug!(url = %record.url, "Found badge record for version");
1354                record
1355            }
1356            Ok(None) => {
1357                tracing::debug!("No badge for specific version, trying preferred badge");
1358                // Try to find any badge
1359                match self.dns_resolver.find_preferred_badge(&fqdn).await {
1360                    Ok(Some(record)) => {
1361                        tracing::debug!(url = %record.url, version = ?record.version, "Using preferred badge");
1362                        record
1363                    }
1364                    Ok(None) => {
1365                        tracing::warn!(fqdn = %fqdn, "No badge record found - not an ANS agent");
1366                        return VerificationOutcome::NotAnsAgent {
1367                            fqdn: fqdn.to_string(),
1368                        };
1369                    }
1370                    Err(e) => {
1371                        tracing::error!(error = %e, "DNS lookup failed");
1372                        return self
1373                            .handle_dns_error(e, &fqdn, &version, client_cert, &ans_name)
1374                            .await;
1375                    }
1376                }
1377            }
1378            Err(e) => {
1379                tracing::error!(error = %e, "DNS lookup failed");
1380                return self
1381                    .handle_dns_error(e, &fqdn, &version, client_cert, &ans_name)
1382                    .await;
1383            }
1384        };
1385
1386        // Validate badge URL domain before fetching
1387        if let Err(e) = validate_badge_domain(self.trusted_ra_domains.as_ref(), &badge_record.url) {
1388            return self
1389                .handle_tlog_error(e, &fqdn, &version, client_cert, &ans_name)
1390                .await;
1391        }
1392
1393        // Fetch badge
1394        tracing::debug!(url = %badge_record.url, "Fetching badge from transparency log");
1395        let badge = match self.tlog_client.fetch_badge(&badge_record.url).await {
1396            Ok(b) => {
1397                tracing::debug!(
1398                    status = ?b.status,
1399                    agent_host = %b.agent_host(),
1400                    ans_name = %b.agent_name(),
1401                    "Fetched badge successfully"
1402                );
1403                b
1404            }
1405            Err(e) => {
1406                tracing::error!(url = %badge_record.url, error = %e, "Failed to fetch badge");
1407                return self
1408                    .handle_tlog_error(e, &fqdn, &version, client_cert, &ans_name)
1409                    .await;
1410            }
1411        };
1412
1413        // Cache the badge (tracked for multi-version lookups)
1414        if let Some(cache) = &self.cache {
1415            cache
1416                .insert_for_fqdn_version(&fqdn, &version, badge.clone())
1417                .await;
1418            tracing::debug!(fqdn = %fqdn, version = %version, "Cached badge");
1419        }
1420
1421        let outcome = self.verify_client_against_badge(&badge, client_cert, &ans_name);
1422        // Refresh-on-mismatch for freshly fetched badges
1423        if matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }) {
1424            tracing::info!(fqdn = %fqdn, "Fingerprint mismatch, attempting refresh");
1425            return self
1426                .verify_client_with_refresh(&fqdn, &version, client_cert, &ans_name)
1427                .await;
1428        }
1429        outcome
1430    }
1431
1432    #[allow(clippy::unused_self)] // logically part of ClientVerifier; may use self in future
1433    fn verify_client_against_badge(
1434        &self,
1435        badge: &Badge,
1436        cert: &CertIdentity,
1437        ans_name: &AnsName,
1438    ) -> VerificationOutcome {
1439        tracing::debug!("Verifying client certificate against badge");
1440
1441        // Check status
1442        if badge.status.should_reject() {
1443            tracing::warn!(status = ?badge.status, "Badge status is not valid for connections");
1444            return VerificationOutcome::InvalidStatus {
1445                status: badge.status,
1446                badge: badge.clone(),
1447            };
1448        }
1449        tracing::debug!(status = ?badge.status, "Badge status is valid");
1450
1451        // Compare fingerprint (identity cert for mTLS clients)
1452        let expected_fp = badge.identity_cert_fingerprint();
1453        tracing::debug!(
1454            expected = %expected_fp,
1455            actual = %cert.fingerprint,
1456            "Comparing identity certificate fingerprints"
1457        );
1458
1459        if !cert.fingerprint.matches(expected_fp) {
1460            tracing::error!(
1461                expected = %expected_fp,
1462                actual = %cert.fingerprint,
1463                "Identity certificate fingerprint MISMATCH"
1464            );
1465            return VerificationOutcome::FingerprintMismatch {
1466                expected: expected_fp.to_string(),
1467                actual: cert.fingerprint.to_string(),
1468                badge: badge.clone(),
1469            };
1470        }
1471        tracing::debug!("Identity fingerprint matches");
1472
1473        // Compare hostname
1474        let expected_host = badge.agent_host();
1475        let actual_host = cert.fqdn().unwrap_or("");
1476        tracing::debug!(
1477            expected = %expected_host,
1478            actual = %actual_host,
1479            "Comparing hostnames"
1480        );
1481
1482        if !actual_host.eq_ignore_ascii_case(expected_host) {
1483            tracing::error!(
1484                expected = %expected_host,
1485                actual = %actual_host,
1486                "Hostname MISMATCH"
1487            );
1488            return VerificationOutcome::HostnameMismatch {
1489                expected: expected_host.to_string(),
1490                actual: actual_host.to_string(),
1491                badge: badge.clone(),
1492            };
1493        }
1494        tracing::debug!("Hostname matches");
1495
1496        // Compare ANS name
1497        let expected_ans_name = badge.agent_name();
1498        tracing::debug!(
1499            expected = %expected_ans_name,
1500            actual = %ans_name,
1501            "Comparing ANS names"
1502        );
1503
1504        if ans_name.to_string() != expected_ans_name {
1505            tracing::error!(
1506                expected = %expected_ans_name,
1507                actual = %ans_name,
1508                "ANS name MISMATCH"
1509            );
1510            return VerificationOutcome::AnsNameMismatch {
1511                expected: expected_ans_name.to_string(),
1512                actual: ans_name.to_string(),
1513                badge: badge.clone(),
1514            };
1515        }
1516
1517        tracing::info!(
1518            agent = %badge.agent_name(),
1519            host = %badge.agent_host(),
1520            "Client verification SUCCESSFUL"
1521        );
1522        VerificationOutcome::Verified {
1523            badge: badge.clone(),
1524            matched_fingerprint: cert.fingerprint.clone(),
1525        }
1526    }
1527
1528    /// Refresh-on-mismatch for client verification.
1529    ///
1530    /// Invalidates the cache, re-fetches the badge from the transparency log,
1531    /// and re-verifies. This handles certificate renewals where the badge
1532    /// was updated but the verifier had a stale copy.
1533    async fn verify_client_with_refresh(
1534        &self,
1535        fqdn: &Fqdn,
1536        version: &Version,
1537        client_cert: &CertIdentity,
1538        ans_name: &AnsName,
1539    ) -> VerificationOutcome {
1540        // Invalidate cache
1541        if let Some(cache) = &self.cache {
1542            cache
1543                .invalidate(&CacheKey::fqdn_version(fqdn, version))
1544                .await;
1545        }
1546
1547        // Re-fetch from DNS
1548        let badge_record = match self
1549            .dns_resolver
1550            .find_badge_for_version(fqdn, version)
1551            .await
1552        {
1553            Ok(Some(record)) => record,
1554            Ok(None) => match self.dns_resolver.find_preferred_badge(fqdn).await {
1555                Ok(Some(record)) => record,
1556                Ok(None) => {
1557                    return VerificationOutcome::NotAnsAgent {
1558                        fqdn: fqdn.to_string(),
1559                    };
1560                }
1561                Err(e) => return VerificationOutcome::DnsError(e),
1562            },
1563            Err(e) => return VerificationOutcome::DnsError(e),
1564        };
1565
1566        // Validate badge URL domain before re-fetch
1567        if let Err(e) = validate_badge_domain(self.trusted_ra_domains.as_ref(), &badge_record.url) {
1568            return VerificationOutcome::TlogError(e);
1569        }
1570
1571        // Re-fetch badge from transparency log
1572        let badge = match self.tlog_client.fetch_badge(&badge_record.url).await {
1573            Ok(b) => b,
1574            Err(e) => return VerificationOutcome::TlogError(e),
1575        };
1576
1577        // Cache the refreshed badge (tracked for multi-version lookups)
1578        if let Some(cache) = &self.cache {
1579            cache
1580                .insert_for_fqdn_version(fqdn, version, badge.clone())
1581                .await;
1582        }
1583
1584        // Re-verify — this is the final answer
1585        self.verify_client_against_badge(&badge, client_cert, ans_name)
1586    }
1587
1588    async fn handle_dns_error(
1589        &self,
1590        error: DnsError,
1591        fqdn: &Fqdn,
1592        version: &Version,
1593        cert: &CertIdentity,
1594        ans_name: &AnsName,
1595    ) -> VerificationOutcome {
1596        match self.failure_policy {
1597            FailurePolicy::FailClosed => VerificationOutcome::DnsError(error),
1598            FailurePolicy::FailOpenWithCache { max_staleness } => {
1599                if let Some(cache) = &self.cache
1600                    && let Some(cached) = cache.get_by_fqdn_version(fqdn, version).await
1601                    && cached.fetched_at.elapsed() < max_staleness
1602                {
1603                    return self.verify_client_against_badge(&cached.badge, cert, ans_name);
1604                }
1605                VerificationOutcome::DnsError(error)
1606            }
1607        }
1608    }
1609
1610    async fn handle_tlog_error(
1611        &self,
1612        error: TlogError,
1613        fqdn: &Fqdn,
1614        version: &Version,
1615        cert: &CertIdentity,
1616        ans_name: &AnsName,
1617    ) -> VerificationOutcome {
1618        match self.failure_policy {
1619            FailurePolicy::FailClosed => VerificationOutcome::TlogError(error),
1620            FailurePolicy::FailOpenWithCache { max_staleness } => {
1621                if let Some(cache) = &self.cache
1622                    && let Some(cached) = cache.get_by_fqdn_version(fqdn, version).await
1623                    && cached.fetched_at.elapsed() < max_staleness
1624                {
1625                    return self.verify_client_against_badge(&cached.badge, cert, ans_name);
1626                }
1627                VerificationOutcome::TlogError(error)
1628            }
1629        }
1630    }
1631}
1632
1633/// Builder for `ClientVerifier`.
1634#[derive(Default)]
1635pub struct ClientVerifierBuilder {
1636    dns_resolver: Option<Arc<dyn DnsResolver>>,
1637    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
1638    cache: Option<Arc<BadgeCache>>,
1639    failure_policy: FailurePolicy,
1640    trusted_ra_domains: Option<HashSet<String>>,
1641}
1642
1643impl fmt::Debug for ClientVerifierBuilder {
1644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1645        f.debug_struct("ClientVerifierBuilder")
1646            .field("failure_policy", &self.failure_policy)
1647            .field("has_dns_resolver", &self.dns_resolver.is_some())
1648            .field("has_tlog_client", &self.tlog_client.is_some())
1649            .field("has_cache", &self.cache.is_some())
1650            .finish_non_exhaustive()
1651    }
1652}
1653
1654impl ClientVerifierBuilder {
1655    /// Set a custom DNS resolver.
1656    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
1657        self.dns_resolver = Some(resolver);
1658        self
1659    }
1660
1661    /// Set a custom transparency log client.
1662    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
1663        self.tlog_client = Some(client);
1664        self
1665    }
1666
1667    /// Enable caching with default configuration.
1668    pub fn with_cache(mut self) -> Self {
1669        self.cache = Some(Arc::new(BadgeCache::with_defaults()));
1670        self
1671    }
1672
1673    /// Enable caching with custom configuration.
1674    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
1675        self.cache = Some(Arc::new(BadgeCache::new(config)));
1676        self
1677    }
1678
1679    /// Use an existing cache.
1680    pub fn cache(mut self, cache: Arc<BadgeCache>) -> Self {
1681        self.cache = Some(cache);
1682        self
1683    }
1684
1685    /// Set the failure policy.
1686    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
1687        self.failure_policy = policy;
1688        self
1689    }
1690
1691    /// Restrict badge URL fetches to a set of trusted RA domains.
1692    ///
1693    /// When configured, badge URLs discovered via DNS TXT records will be
1694    /// validated against this set before any HTTP request is made. URLs
1695    /// pointing to hosts not in the set are rejected with
1696    /// `TlogError::UntrustedDomain`.
1697    ///
1698    /// By default (`None`), all domains are allowed.
1699    pub fn trusted_ra_domains(
1700        mut self,
1701        domains: impl IntoIterator<Item = impl Into<String>>,
1702    ) -> Self {
1703        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
1704        self
1705    }
1706
1707    /// Build the verifier.
1708    pub async fn build(self) -> AnsResult<ClientVerifier> {
1709        let dns_resolver = match self.dns_resolver {
1710            Some(r) => r,
1711            None => Arc::new(
1712                HickoryDnsResolver::new()
1713                    .await
1714                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
1715            ),
1716        };
1717
1718        let tlog_client = self
1719            .tlog_client
1720            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
1721
1722        Ok(ClientVerifier {
1723            dns_resolver,
1724            tlog_client,
1725            cache: self.cache,
1726            failure_policy: self.failure_policy,
1727            trusted_ra_domains: self.trusted_ra_domains,
1728        })
1729    }
1730}
1731
1732/// High-level ANS verifier combining server and client verification.
1733pub struct AnsVerifier {
1734    server_verifier: ServerVerifier,
1735    client_verifier: ClientVerifier,
1736    #[cfg(feature = "rustls")]
1737    private_ca_pem: Option<Vec<u8>>,
1738    #[cfg(feature = "scitt")]
1739    scitt_config: Option<ScittConfig>,
1740    #[cfg(feature = "scitt")]
1741    scitt_key_store: Option<Arc<crate::scitt::RefreshableKeyStore>>,
1742    #[cfg(feature = "scitt")]
1743    scitt_verification_cache: Option<Arc<crate::scitt::ScittVerificationCache>>,
1744}
1745
1746impl fmt::Debug for AnsVerifier {
1747    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1748        let builder = &mut f.debug_struct("AnsVerifier");
1749        builder
1750            .field("server_verifier", &self.server_verifier)
1751            .field("client_verifier", &self.client_verifier);
1752        #[cfg(feature = "scitt")]
1753        builder.field("has_scitt_config", &self.scitt_config.is_some());
1754        #[cfg(feature = "scitt")]
1755        builder.field(
1756            "has_scitt_verification_cache",
1757            &self.scitt_verification_cache.is_some(),
1758        );
1759        builder.finish_non_exhaustive()
1760    }
1761}
1762
1763impl AnsVerifier {
1764    /// Create a new verifier with default configuration.
1765    pub async fn new() -> AnsResult<Self> {
1766        Self::builder().build().await
1767    }
1768
1769    /// Create a builder for custom configuration.
1770    pub fn builder() -> AnsVerifierBuilder {
1771        AnsVerifierBuilder::default()
1772    }
1773
1774    /// Verify an agent server (client-side verification).
1775    pub async fn verify_server(
1776        &self,
1777        fqdn: impl AsRef<str>,
1778        server_cert: &CertIdentity,
1779    ) -> VerificationOutcome {
1780        let fqdn = match Fqdn::new(fqdn.as_ref()) {
1781            Ok(f) => f,
1782            Err(e) => return VerificationOutcome::ParseError(e),
1783        };
1784        self.server_verifier.verify(&fqdn, server_cert).await
1785    }
1786
1787    /// Verify an mTLS client (server-side verification).
1788    pub async fn verify_client(&self, client_cert: &CertIdentity) -> VerificationOutcome {
1789        self.client_verifier.verify(client_cert).await
1790    }
1791
1792    /// Pre-fetch a badge for caching (before TLS connection).
1793    pub async fn prefetch(&self, fqdn: impl AsRef<str>) -> AnsResult<Badge> {
1794        let fqdn = Fqdn::new(fqdn.as_ref())?;
1795        self.server_verifier.prefetch(&fqdn).await
1796    }
1797
1798    /// Create a rustls [`AnsClientCertVerifier`](crate::AnsClientCertVerifier) for server-side mTLS.
1799    ///
1800    /// Requires `private_ca_pem` to have been set on the builder.
1801    /// Returns a verifier that validates client certificate chains against the ANS Private CA.
1802    /// Client certificates are **required**.
1803    #[cfg(feature = "rustls")]
1804    pub fn client_cert_verifier(&self) -> AnsResult<crate::AnsClientCertVerifier> {
1805        let pem = self.private_ca_pem.as_ref().ok_or_else(|| {
1806            AnsError::Verification(VerificationError::Configuration(
1807                "private_ca_pem is required for client_cert_verifier".into(),
1808            ))
1809        })?;
1810        crate::AnsClientCertVerifier::from_pem(pem).map_err(|e| {
1811            AnsError::Verification(VerificationError::Configuration(format!(
1812                "Failed to build client cert verifier: {e}"
1813            )))
1814        })
1815    }
1816
1817    /// Create a rustls [`AnsClientCertVerifier`](crate::AnsClientCertVerifier) that allows optional client certs.
1818    ///
1819    /// Requires `private_ca_pem` to have been set on the builder.
1820    /// Use this when the server should accept both authenticated and unauthenticated clients.
1821    #[cfg(feature = "rustls")]
1822    pub fn client_cert_verifier_optional(&self) -> AnsResult<crate::AnsClientCertVerifier> {
1823        let pem = self.private_ca_pem.as_ref().ok_or_else(|| {
1824            AnsError::Verification(VerificationError::Configuration(
1825                "private_ca_pem is required for client_cert_verifier_optional".into(),
1826            ))
1827        })?;
1828        crate::AnsClientCertVerifier::from_pem_optional(pem).map_err(|e| {
1829            AnsError::Verification(VerificationError::Configuration(format!(
1830                "Failed to build optional client cert verifier: {e}"
1831            )))
1832        })
1833    }
1834
1835    /// Create a rustls [`AnsServerCertVerifier`](crate::AnsServerCertVerifier) for a specific badge fingerprint.
1836    ///
1837    /// Typically called after [`prefetch()`](Self::prefetch) to get the expected fingerprint
1838    /// from the badge's `attestations.server_cert.fingerprint` field.
1839    #[cfg(feature = "rustls")]
1840    pub fn server_cert_verifier(
1841        &self,
1842        fingerprint: &CertFingerprint,
1843    ) -> AnsResult<crate::AnsServerCertVerifier> {
1844        crate::AnsServerCertVerifier::new(fingerprint.clone()).map_err(|e| {
1845            AnsError::Verification(VerificationError::Configuration(format!(
1846                "Failed to build server cert verifier: {e}"
1847            )))
1848        })
1849    }
1850
1851    /// Verify an agent server with SCITT artifacts from HTTP headers.
1852    ///
1853    /// This implements the SCITT verification flow:
1854    /// 1. If SCITT headers are present, verify status token signature + expiry + cert fingerprint
1855    /// 2. If receipt is also present, verify Merkle inclusion proof → `FullScitt` tier
1856    /// 3. If headers are absent, fall back to badge-based verification (per `ScittTierPolicy`)
1857    ///
1858    /// **Present headers are final**: if SCITT headers are present but
1859    /// invalid/expired/corrupt, the result is a hard reject — badge
1860    /// verification is never used as fallback. This prevents MITM downgrade.
1861    ///
1862    /// If `scitt_config` was not set on the builder, falls through to
1863    /// badge-based `verify_server`.
1864    #[cfg(feature = "scitt")]
1865    pub async fn verify_server_with_scitt(
1866        &self,
1867        fqdn: impl AsRef<str>,
1868        server_cert: &CertIdentity,
1869        headers: &crate::scitt::ScittHeaders,
1870    ) -> VerificationOutcome {
1871        let fqdn_str = fqdn.as_ref();
1872        let parsed_fqdn = match Fqdn::new(fqdn_str) {
1873            Ok(f) => f,
1874            Err(e) => return VerificationOutcome::ParseError(e),
1875        };
1876
1877        let Some(config) = &self.scitt_config else {
1878            // SCITT not configured — fall through to badge
1879            return self.server_verifier.verify(&parsed_fqdn, server_cert).await;
1880        };
1881
1882        let Some(key_store) = &self.scitt_key_store else {
1883            // Defensive: builder.build() rejects this combination, but guard
1884            // against direct struct construction in tests.
1885            tracing::error!("BUG: scitt_config present but no key store — falling back to badge");
1886            return self.server_verifier.verify(&parsed_fqdn, server_cert).await;
1887        };
1888
1889        match config.tier_policy {
1890            ScittTierPolicy::ScittWithBadgeFallback => {
1891                self.verify_scitt_first(&parsed_fqdn, server_cert, headers, key_store, config, true)
1892                    .await
1893            }
1894            ScittTierPolicy::RequireScitt => {
1895                self.verify_scitt_first(
1896                    &parsed_fqdn,
1897                    server_cert,
1898                    headers,
1899                    key_store,
1900                    config,
1901                    false,
1902                )
1903                .await
1904            }
1905            ScittTierPolicy::BadgeWithScittEnhancement => {
1906                // Badge first, then optionally enhance with SCITT
1907                let badge_outcome = self.server_verifier.verify(&parsed_fqdn, server_cert).await;
1908                if !badge_outcome.is_success() || headers.is_empty() {
1909                    return badge_outcome;
1910                }
1911                // Badge succeeded and SCITT headers present — try to upgrade.
1912                // If SCITT headers are present but corrupt, REJECT (no fallback
1913                // to badge) — prevents MITM downgrade via header injection.
1914                let scitt_cache = self.scitt_verification_cache.as_deref();
1915                let scitt_outcome = Self::try_scitt_verification(
1916                    server_cert,
1917                    headers,
1918                    key_store,
1919                    config,
1920                    true,
1921                    scitt_cache,
1922                )
1923                .await;
1924                match scitt_outcome {
1925                    Some(VerificationOutcome::ScittVerified {
1926                        status_token,
1927                        tier,
1928                        matched_fingerprint,
1929                        badge: _,
1930                    }) => {
1931                        // Carry the badge from badge_outcome into ScittVerified
1932                        let badge = badge_outcome.badge().cloned();
1933                        VerificationOutcome::ScittVerified {
1934                            status_token,
1935                            tier,
1936                            matched_fingerprint,
1937                            badge,
1938                        }
1939                    }
1940                    // Any SCITT failure when headers are present = hard reject.
1941                    // Present-but-corrupt headers must never fall back to badge.
1942                    Some(outcome) => outcome,
1943                    // None = no status token in headers (shouldn't happen since
1944                    // we checked !headers.is_empty() above, but defensively
1945                    // return the badge outcome).
1946                    None => badge_outcome,
1947                }
1948            }
1949        }
1950    }
1951
1952    /// Verify an mTLS client with SCITT artifacts from HTTP headers.
1953    ///
1954    /// Same SCITT-first flow as server verification, but matches against
1955    /// identity certificate fingerprints instead of server certificates.
1956    #[cfg(feature = "scitt")]
1957    pub async fn verify_client_with_scitt(
1958        &self,
1959        client_cert: &CertIdentity,
1960        headers: &crate::scitt::ScittHeaders,
1961    ) -> VerificationOutcome {
1962        let Some(config) = &self.scitt_config else {
1963            return self.client_verifier.verify(client_cert).await;
1964        };
1965
1966        let Some(key_store) = &self.scitt_key_store else {
1967            // Defensive: builder.build() rejects this combination, but guard
1968            // against direct struct construction in tests.
1969            tracing::error!("BUG: scitt_config present but no key store — falling back to badge");
1970            return self.client_verifier.verify(client_cert).await;
1971        };
1972
1973        match config.tier_policy {
1974            ScittTierPolicy::ScittWithBadgeFallback => {
1975                self.verify_client_scitt_first(client_cert, headers, key_store, config, true)
1976                    .await
1977            }
1978            ScittTierPolicy::RequireScitt => {
1979                self.verify_client_scitt_first(client_cert, headers, key_store, config, false)
1980                    .await
1981            }
1982            ScittTierPolicy::BadgeWithScittEnhancement => {
1983                let badge_outcome = self.client_verifier.verify(client_cert).await;
1984                if !badge_outcome.is_success() || headers.is_empty() {
1985                    return badge_outcome;
1986                }
1987                // Headers present — try SCITT upgrade. Corrupt headers = reject.
1988                let scitt_cache = self.scitt_verification_cache.as_deref();
1989                let scitt_outcome = Self::try_scitt_verification(
1990                    client_cert,
1991                    headers,
1992                    key_store,
1993                    config,
1994                    false,
1995                    scitt_cache,
1996                )
1997                .await;
1998                match scitt_outcome {
1999                    Some(VerificationOutcome::ScittVerified {
2000                        status_token,
2001                        tier,
2002                        matched_fingerprint,
2003                        badge: _,
2004                    }) => {
2005                        let badge = badge_outcome.badge().cloned();
2006                        VerificationOutcome::ScittVerified {
2007                            status_token,
2008                            tier,
2009                            matched_fingerprint,
2010                            badge,
2011                        }
2012                    }
2013                    Some(outcome) => outcome, // present-but-corrupt = reject
2014                    None => badge_outcome,
2015                }
2016            }
2017        }
2018    }
2019
2020    /// SCITT-first verification: try SCITT, optionally fall back to badge
2021    /// **only when headers are completely absent**.
2022    ///
2023    /// When headers are present, the SCITT result is final — no badge
2024    /// fallback for any error, including `TokenExpired`. This prevents
2025    /// MITM downgrade via header injection/corruption.
2026    #[cfg(feature = "scitt")]
2027    async fn verify_scitt_first(
2028        &self,
2029        fqdn: &Fqdn,
2030        server_cert: &CertIdentity,
2031        headers: &crate::scitt::ScittHeaders,
2032        key_store: &Arc<crate::scitt::RefreshableKeyStore>,
2033        config: &ScittConfig,
2034        allow_badge_fallback: bool,
2035    ) -> VerificationOutcome {
2036        // If no SCITT headers at all, the peer doesn't support SCITT
2037        if headers.is_empty() {
2038            if allow_badge_fallback {
2039                tracing::debug!(fqdn = %fqdn, "No SCITT headers — falling back to badge");
2040                return self.server_verifier.verify(fqdn, server_cert).await;
2041            }
2042            return VerificationOutcome::ScittError(crate::scitt::ScittError::MissingTokenField(
2043                "No SCITT headers present and RequireScitt policy is active".to_string(),
2044            ));
2045        }
2046
2047        // Headers are present — SCITT result is final, no badge fallback.
2048        let scitt_cache = self.scitt_verification_cache.as_deref();
2049        match Self::try_scitt_verification(
2050            server_cert,
2051            headers,
2052            key_store,
2053            config,
2054            true,
2055            scitt_cache,
2056        )
2057        .await
2058        {
2059            Some(outcome) => outcome,
2060            None => {
2061                // Headers present but no status token decoded — hard reject.
2062                VerificationOutcome::ScittError(crate::scitt::ScittError::MissingTokenField(
2063                    "SCITT headers present but no valid status token found".to_string(),
2064                ))
2065            }
2066        }
2067    }
2068
2069    /// SCITT-first client verification with optional badge fallback
2070    /// **only when headers are completely absent**.
2071    #[cfg(feature = "scitt")]
2072    async fn verify_client_scitt_first(
2073        &self,
2074        client_cert: &CertIdentity,
2075        headers: &crate::scitt::ScittHeaders,
2076        key_store: &Arc<crate::scitt::RefreshableKeyStore>,
2077        config: &ScittConfig,
2078        allow_badge_fallback: bool,
2079    ) -> VerificationOutcome {
2080        if headers.is_empty() {
2081            if allow_badge_fallback {
2082                tracing::debug!("No SCITT headers on client — falling back to badge");
2083                return self.client_verifier.verify(client_cert).await;
2084            }
2085            return VerificationOutcome::ScittError(crate::scitt::ScittError::MissingTokenField(
2086                "No SCITT headers present and RequireScitt policy is active".to_string(),
2087            ));
2088        }
2089
2090        // Headers are present — SCITT result is final, no badge fallback.
2091        let scitt_cache = self.scitt_verification_cache.as_deref();
2092        match Self::try_scitt_verification(
2093            client_cert,
2094            headers,
2095            key_store,
2096            config,
2097            false,
2098            scitt_cache,
2099        )
2100        .await
2101        {
2102            Some(outcome) => outcome,
2103            None => VerificationOutcome::ScittError(crate::scitt::ScittError::MissingTokenField(
2104                "SCITT headers present but no valid status token found".to_string(),
2105            )),
2106        }
2107    }
2108
2109    /// Attempt SCITT verification from headers. Returns `None` if no status token.
2110    ///
2111    /// Uses a two-layer caching strategy to avoid redundant cryptographic work:
2112    /// - **Layer 2**: Full outcome cache keyed by `(cert_fp, token_hash, receipt_hash?)`.
2113    ///   A hit returns immediately with zero crypto.
2114    /// - **Layer 1**: Content-addressed token and receipt caches. A hit skips
2115    ///   ECDSA signature verification (~1ms saved per hit).
2116    ///
2117    /// On [`UnknownKeyId`](crate::scitt::ScittError::UnknownKeyId), triggers
2118    /// an on-demand key refresh (respects cooldown) and retries once. This
2119    /// handles key rotations between periodic refresh cycles without allowing
2120    /// amplification attacks from garbage `kid` values.
2121    ///
2122    /// Only successful verifications are cached. Errors are never stored.
2123    ///
2124    /// The `is_server` flag controls which cert array to match:
2125    /// - `true`: matches against `valid_server_certs`
2126    /// - `false`: matches against `valid_identity_certs`
2127    #[cfg(feature = "scitt")]
2128    #[allow(clippy::too_many_lines)] // verification + caching flow reads best as a single method
2129    async fn try_scitt_verification(
2130        cert: &CertIdentity,
2131        headers: &crate::scitt::ScittHeaders,
2132        key_store: &Arc<crate::scitt::RefreshableKeyStore>,
2133        config: &ScittConfig,
2134        is_server: bool,
2135        cache: Option<&crate::scitt::ScittVerificationCache>,
2136    ) -> Option<VerificationOutcome> {
2137        let token_bytes = headers.status_token.as_ref()?;
2138
2139        // Compute content hashes for cache lookups (cheap: ~1μs each)
2140        let token_hash = crate::scitt::hash_bytes(token_bytes);
2141        let receipt_hash = headers
2142            .receipt
2143            .as_ref()
2144            .map(|b| crate::scitt::hash_bytes(b));
2145
2146        // ── Layer 2: Full outcome cache ─────────────────────────────────
2147        if let Some(cache) = cache
2148            && let Some(outcome) = cache
2149                .get_outcome(cert.fingerprint(), &token_hash, receipt_hash.as_ref())
2150                .await
2151        {
2152            tracing::debug!("SCITT verification cache hit (Layer 2 — full outcome)");
2153            return Some(VerificationOutcome::ScittVerified {
2154                status_token: (*outcome.verified_token).clone(),
2155                tier: outcome.tier,
2156                matched_fingerprint: outcome.matched_fingerprint.clone(),
2157                badge: None,
2158            });
2159        }
2160
2161        // ── Layer 1: Token verification cache ───────────────────────────
2162        let verified_token = if let Some(cached_token) = match cache {
2163            Some(c) => c.get_verified_token(&token_hash).await,
2164            None => None,
2165        } {
2166            tracing::debug!("SCITT token cache hit (Layer 1 — skipping ECDSA)");
2167            (*cached_token).clone()
2168        } else {
2169            // Full COSE signature verification
2170            let snapshot = key_store.current_snapshot().await;
2171            let first_result = crate::scitt::verify_status_token(
2172                token_bytes,
2173                &snapshot,
2174                config.clock_skew_tolerance,
2175            );
2176
2177            // On UnknownKeyId: attempt on-demand refresh (respects cooldown) and retry once
2178            let vt = match first_result {
2179                Err(original_err @ crate::scitt::ScittError::UnknownKeyId(_)) => {
2180                    let refreshed = match key_store.refresh_if_cooldown_elapsed().await {
2181                        Ok(did_refresh) => did_refresh,
2182                        Err(refresh_err) => {
2183                            tracing::warn!(error = %refresh_err, "On-demand key refresh failed");
2184                            false
2185                        }
2186                    };
2187
2188                    if refreshed {
2189                        let new_snapshot = key_store.current_snapshot().await;
2190                        match crate::scitt::verify_status_token(
2191                            token_bytes,
2192                            &new_snapshot,
2193                            config.clock_skew_tolerance,
2194                        ) {
2195                            Ok(vt) => vt,
2196                            Err(e) => return Some(VerificationOutcome::ScittError(e)),
2197                        }
2198                    } else {
2199                        return Some(VerificationOutcome::ScittError(original_err));
2200                    }
2201                }
2202                Ok(vt) => vt,
2203                Err(e) => return Some(VerificationOutcome::ScittError(e)),
2204            };
2205
2206            // Store in Layer 1 token cache
2207            if let Some(cache) = cache {
2208                cache
2209                    .insert_verified_token(token_hash, Arc::new(vt.clone()))
2210                    .await;
2211            }
2212
2213            vt
2214        };
2215
2216        // ── Fingerprint comparison (always, cheap) ──────────────────────
2217        let fingerprint_matches = if is_server {
2218            crate::scitt::matches_server_cert(&verified_token.payload, cert.fingerprint())
2219        } else {
2220            crate::scitt::matches_identity_cert(&verified_token.payload, cert.fingerprint())
2221        };
2222
2223        if !fingerprint_matches {
2224            return Some(VerificationOutcome::ScittError(
2225                crate::scitt::ScittError::MissingTokenField(format!(
2226                    "Certificate fingerprint {} not found in status token's {} cert list ({} entries)",
2227                    cert.fingerprint(),
2228                    if is_server { "server" } else { "identity" },
2229                    if is_server {
2230                        verified_token.payload.valid_server_certs.len()
2231                    } else {
2232                        verified_token.payload.valid_identity_certs.len()
2233                    }
2234                )),
2235            ));
2236        }
2237
2238        // ── Receipt verification (Layer 1 cached) ──────────────────────
2239        let tier = if let Some(receipt_bytes) = &headers.receipt {
2240            // Safety: receipt_hash is always Some when receipt bytes are present
2241            // (computed at the top of this function from the same Option).
2242            let Some(rh) = receipt_hash.as_ref() else {
2243                // Defensive: should be unreachable, but fall back gracefully
2244                tracing::warn!("receipt_hash missing despite receipt bytes present");
2245                return Some(VerificationOutcome::ScittError(
2246                    crate::scitt::ScittError::MissingTokenField(
2247                        "Internal error: receipt hash not computed".to_string(),
2248                    ),
2249                ));
2250            };
2251
2252            if let Some(_cached_receipt) = match cache {
2253                Some(c) => c.get_verified_receipt(rh).await,
2254                None => None,
2255            } {
2256                tracing::debug!("SCITT receipt cache hit (Layer 1 — skipping Merkle)");
2257                ans_types::VerificationTier::FullScitt
2258            } else {
2259                // Full receipt verification — needs a key store snapshot
2260                let snapshot = key_store.current_snapshot().await;
2261                match crate::scitt::verify_receipt(receipt_bytes, &snapshot) {
2262                    Ok(receipt) => {
2263                        tracing::debug!("SCITT receipt verified — FullScitt tier");
2264                        if let Some(cache) = cache {
2265                            cache.insert_verified_receipt(*rh, Arc::new(receipt)).await;
2266                        }
2267                        ans_types::VerificationTier::FullScitt
2268                    }
2269                    Err(e) => {
2270                        if matches!(config.tier_policy, ScittTierPolicy::RequireScitt) {
2271                            tracing::error!(error = %e, "Receipt verification failed under RequireScitt — rejecting");
2272                            return Some(VerificationOutcome::ScittError(e));
2273                        }
2274                        tracing::warn!(error = %e, "Receipt verification failed — StatusTokenVerified tier");
2275                        ans_types::VerificationTier::StatusTokenVerified
2276                    }
2277                }
2278            }
2279        } else {
2280            ans_types::VerificationTier::StatusTokenVerified
2281        };
2282
2283        // ── Store in Layer 2 outcome cache ──────────────────────────────
2284        if let Some(cache) = cache {
2285            cache
2286                .insert_outcome(
2287                    cert.fingerprint(),
2288                    &token_hash,
2289                    receipt_hash.as_ref(),
2290                    crate::scitt::CachedScittOutcome {
2291                        verified_token: Arc::new(verified_token.clone()),
2292                        tier,
2293                        matched_fingerprint: cert.fingerprint().clone(),
2294                        exp: verified_token.payload.exp,
2295                    },
2296                )
2297                .await;
2298        }
2299
2300        Some(VerificationOutcome::ScittVerified {
2301            status_token: verified_token,
2302            tier,
2303            matched_fingerprint: cert.fingerprint().clone(),
2304            badge: None,
2305        })
2306    }
2307}
2308
2309/// Builder for `AnsVerifier`.
2310#[derive(Default)]
2311pub struct AnsVerifierBuilder {
2312    dns_resolver: Option<Arc<dyn DnsResolver>>,
2313    dns_config: Option<DnsResolverConfig>,
2314    dns_nameservers: Option<Vec<std::net::Ipv4Addr>>,
2315    tlog_client: Option<Arc<dyn TransparencyLogClient>>,
2316    cache_config: Option<CacheConfig>,
2317    failure_policy: FailurePolicy,
2318    dane_policy: DanePolicy,
2319    dane_port: Option<u16>,
2320    trusted_ra_domains: Option<HashSet<String>>,
2321    #[cfg(feature = "rustls")]
2322    private_ca_pem: Option<Vec<u8>>,
2323    #[cfg(feature = "scitt")]
2324    scitt_config: Option<ScittConfig>,
2325    #[cfg(feature = "scitt")]
2326    scitt_key_store: Option<Arc<crate::scitt::RefreshableKeyStore>>,
2327    #[cfg(feature = "scitt")]
2328    scitt_verification_cache: Option<Arc<crate::scitt::ScittVerificationCache>>,
2329}
2330
2331impl fmt::Debug for AnsVerifierBuilder {
2332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2333        let builder = &mut f.debug_struct("AnsVerifierBuilder");
2334        builder
2335            .field("dns_config", &self.dns_config)
2336            .field("failure_policy", &self.failure_policy)
2337            .field("dane_policy", &self.dane_policy)
2338            .field("dane_port", &self.dane_port)
2339            .field("has_dns_resolver", &self.dns_resolver.is_some())
2340            .field("has_tlog_client", &self.tlog_client.is_some())
2341            .field("has_cache_config", &self.cache_config.is_some());
2342        #[cfg(feature = "scitt")]
2343        builder
2344            .field("has_scitt_config", &self.scitt_config.is_some())
2345            .field("has_scitt_key_store", &self.scitt_key_store.is_some());
2346        builder.finish_non_exhaustive()
2347    }
2348}
2349
2350impl AnsVerifierBuilder {
2351    /// Set a custom DNS resolver.
2352    pub fn dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
2353        self.dns_resolver = Some(resolver);
2354        self
2355    }
2356
2357    /// Use a preset DNS resolver configuration (Cloudflare, Google, etc.).
2358    ///
2359    /// # Example
2360    /// ```rust,no_run
2361    /// use ans_verify::{AnsVerifier, DnsResolverConfig};
2362    ///
2363    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2364    /// let verifier = AnsVerifier::builder()
2365    ///     .dns_preset(DnsResolverConfig::CloudflareTls)
2366    ///     .build()
2367    ///     .await?;
2368    /// # Ok(())
2369    /// # }
2370    /// ```
2371    pub fn dns_preset(mut self, preset: DnsResolverConfig) -> Self {
2372        self.dns_config = Some(preset);
2373        self
2374    }
2375
2376    /// Use Cloudflare DNS (1.1.1.1).
2377    pub fn dns_cloudflare(self) -> Self {
2378        self.dns_preset(DnsResolverConfig::Cloudflare)
2379    }
2380
2381    /// Use Cloudflare DNS over TLS.
2382    pub fn dns_cloudflare_tls(self) -> Self {
2383        self.dns_preset(DnsResolverConfig::CloudflareTls)
2384    }
2385
2386    /// Use Google Public DNS (8.8.8.8).
2387    pub fn dns_google(self) -> Self {
2388        self.dns_preset(DnsResolverConfig::Google)
2389    }
2390
2391    /// Use Google DNS over TLS.
2392    pub fn dns_google_tls(self) -> Self {
2393        self.dns_preset(DnsResolverConfig::GoogleTls)
2394    }
2395
2396    /// Use Quad9 DNS (9.9.9.9).
2397    pub fn dns_quad9(self) -> Self {
2398        self.dns_preset(DnsResolverConfig::Quad9)
2399    }
2400
2401    /// Use custom DNS nameservers.
2402    ///
2403    /// # Example
2404    /// ```rust,no_run
2405    /// use ans_verify::AnsVerifier;
2406    /// use std::net::Ipv4Addr;
2407    ///
2408    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2409    /// let verifier = AnsVerifier::builder()
2410    ///     .dns_nameservers(&[
2411    ///         Ipv4Addr::new(1, 1, 1, 1),
2412    ///         Ipv4Addr::new(8, 8, 8, 8),
2413    ///     ])
2414    ///     .build()
2415    ///     .await?;
2416    /// # Ok(())
2417    /// # }
2418    /// ```
2419    pub fn dns_nameservers(mut self, nameservers: &[std::net::Ipv4Addr]) -> Self {
2420        self.dns_nameservers = Some(nameservers.to_vec());
2421        self
2422    }
2423
2424    /// Set a custom transparency log client.
2425    pub fn tlog_client(mut self, client: Arc<dyn TransparencyLogClient>) -> Self {
2426        self.tlog_client = Some(client);
2427        self
2428    }
2429
2430    /// Enable caching with default configuration.
2431    pub fn with_caching(mut self) -> Self {
2432        self.cache_config = Some(CacheConfig::default());
2433        #[cfg(feature = "scitt")]
2434        if self.scitt_verification_cache.is_none() {
2435            self.scitt_verification_cache = Some(Arc::new(
2436                crate::scitt::ScittVerificationCache::with_defaults(),
2437            ));
2438        }
2439        self
2440    }
2441
2442    /// Enable caching with custom configuration.
2443    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
2444        self.cache_config = Some(config);
2445        self
2446    }
2447
2448    /// Set the failure policy.
2449    pub fn failure_policy(mut self, policy: FailurePolicy) -> Self {
2450        self.failure_policy = policy;
2451        self
2452    }
2453
2454    /// Set the DANE/TLSA verification policy.
2455    ///
2456    /// - `DanePolicy::Disabled`: Skip DANE verification entirely (default)
2457    /// - `DanePolicy::ValidateIfPresent`: Verify TLSA if records exist, skip if not
2458    /// - `DanePolicy::Required`: Require TLSA records to exist and match
2459    pub fn dane_policy(mut self, policy: DanePolicy) -> Self {
2460        self.dane_policy = policy;
2461        self
2462    }
2463
2464    /// Enable DANE verification if TLSA records are present.
2465    pub fn with_dane_if_present(mut self) -> Self {
2466        self.dane_policy = DanePolicy::ValidateIfPresent;
2467        self
2468    }
2469
2470    /// Require DANE verification (fail if no TLSA records).
2471    pub fn require_dane(mut self) -> Self {
2472        self.dane_policy = DanePolicy::Required;
2473        self
2474    }
2475
2476    /// Set the port for TLSA lookups (default: 443).
2477    pub fn dane_port(mut self, port: u16) -> Self {
2478        self.dane_port = Some(port);
2479        self
2480    }
2481
2482    /// Restrict badge URL fetches to a set of trusted RA domains.
2483    ///
2484    /// When configured, badge URLs discovered via DNS TXT records will be
2485    /// validated against this set before any HTTP request is made.
2486    pub fn trusted_ra_domains(
2487        mut self,
2488        domains: impl IntoIterator<Item = impl Into<String>>,
2489    ) -> Self {
2490        self.trusted_ra_domains = Some(domains.into_iter().map(Into::into).collect());
2491        self
2492    }
2493
2494    /// Set the ANS Private CA certificate (PEM-encoded).
2495    ///
2496    /// Required for mTLS client verification. The Private CA is used during
2497    /// the TLS handshake to validate that client certificates chain to the
2498    /// ANS Private CA. Different environments (OTE, PROD) use different CAs.
2499    ///
2500    /// The PEM bytes are typically loaded from configuration, not hardcoded.
2501    #[cfg(feature = "rustls")]
2502    pub fn private_ca_pem(mut self, pem: impl Into<Vec<u8>>) -> Self {
2503        self.private_ca_pem = Some(pem.into());
2504        self
2505    }
2506
2507    /// Enable SCITT verification with the given configuration.
2508    ///
2509    /// When set, the `verify_server_with_scitt` and `verify_client_with_scitt`
2510    /// methods become available on the resulting `AnsVerifier`.
2511    #[cfg(feature = "scitt")]
2512    pub fn scitt_config(mut self, config: ScittConfig) -> Self {
2513        self.scitt_config = Some(config);
2514        self
2515    }
2516
2517    /// Pre-configure SCITT root keys (static, no refresh).
2518    ///
2519    /// Use this for tests or offline environments where root keys are known
2520    /// ahead of time. The keys are wrapped in a static
2521    /// [`RefreshableKeyStore`](crate::scitt::RefreshableKeyStore) with no
2522    /// refresh capability.
2523    ///
2524    /// For production use with automatic key refresh, use
2525    /// [`scitt_refreshable_key_store`](Self::scitt_refreshable_key_store).
2526    #[cfg(feature = "scitt")]
2527    #[allow(clippy::needless_pass_by_value)] // Arc param is intentional public API
2528    pub fn scitt_key_store(mut self, key_store: Arc<crate::scitt::ScittKeyStore>) -> Self {
2529        self.scitt_key_store = Some(Arc::new(crate::scitt::RefreshableKeyStore::from_static(
2530            (*key_store).clone(),
2531        )));
2532        self
2533    }
2534
2535    /// Configure a refreshable SCITT root key store with periodic and
2536    /// on-demand refresh support.
2537    ///
2538    /// Use this for production environments. See
2539    /// [`RefreshableKeyStore`](crate::scitt::RefreshableKeyStore) for details
2540    /// on refresh behavior and anti-amplification cooldown.
2541    #[cfg(feature = "scitt")]
2542    pub fn scitt_refreshable_key_store(
2543        mut self,
2544        key_store: Arc<crate::scitt::RefreshableKeyStore>,
2545    ) -> Self {
2546        self.scitt_key_store = Some(key_store);
2547        self
2548    }
2549
2550    /// Configure a custom SCITT verification cache.
2551    ///
2552    /// Overrides the default cache created by
2553    /// [`with_caching`](Self::with_caching). Use this when you need
2554    /// custom cache sizing.
2555    #[cfg(feature = "scitt")]
2556    pub fn with_scitt_verification_cache(
2557        mut self,
2558        cache: crate::scitt::ScittVerificationCache,
2559    ) -> Self {
2560        self.scitt_verification_cache = Some(Arc::new(cache));
2561        self
2562    }
2563
2564    /// Build the verifier.
2565    ///
2566    /// # Errors
2567    ///
2568    /// Returns [`VerificationError::Configuration`] if `scitt_config` is set
2569    /// without a key store. Use [`scitt_key_store`](Self::scitt_key_store) or
2570    /// [`scitt_refreshable_key_store`](Self::scitt_refreshable_key_store) to
2571    /// provide one.
2572    pub async fn build(self) -> AnsResult<AnsVerifier> {
2573        // Validate SCITT configuration: config requires a key store.
2574        #[cfg(feature = "scitt")]
2575        if self.scitt_config.is_some() && self.scitt_key_store.is_none() {
2576            return Err(AnsError::Verification(VerificationError::Configuration(
2577                "scitt_config requires a key store — call scitt_key_store() or \
2578                 scitt_refreshable_key_store() on the builder"
2579                    .to_string(),
2580            )));
2581        }
2582
2583        // Determine DNS resolver: custom > nameservers > preset > default
2584        let dns_resolver: Arc<dyn DnsResolver> = if let Some(r) = self.dns_resolver {
2585            r
2586        } else if let Some(nameservers) = self.dns_nameservers {
2587            Arc::new(
2588                HickoryDnsResolver::with_nameservers(&nameservers)
2589                    .await
2590                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
2591            )
2592        } else if let Some(preset) = self.dns_config {
2593            Arc::new(
2594                HickoryDnsResolver::with_preset(preset)
2595                    .await
2596                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
2597            )
2598        } else {
2599            Arc::new(
2600                HickoryDnsResolver::new()
2601                    .await
2602                    .map_err(|e| AnsError::Dns(DnsError::ResolverError(e.to_string())))?,
2603            )
2604        };
2605
2606        let tlog_client: Arc<dyn TransparencyLogClient> = self
2607            .tlog_client
2608            .unwrap_or_else(|| Arc::new(HttpTransparencyLogClient::new()));
2609
2610        let cache = self.cache_config.map(|c| Arc::new(BadgeCache::new(c)));
2611        let dane_port = self.dane_port.unwrap_or(443);
2612
2613        let server_verifier = ServerVerifier {
2614            dns_resolver: dns_resolver.clone(),
2615            tlog_client: tlog_client.clone(),
2616            cache: cache.clone(),
2617            failure_policy: self.failure_policy,
2618            dane_policy: self.dane_policy,
2619            dane_port,
2620            trusted_ra_domains: self.trusted_ra_domains.clone(),
2621        };
2622
2623        let client_verifier = ClientVerifier {
2624            dns_resolver,
2625            tlog_client,
2626            cache,
2627            failure_policy: self.failure_policy,
2628            trusted_ra_domains: self.trusted_ra_domains,
2629        };
2630
2631        Ok(AnsVerifier {
2632            server_verifier,
2633            client_verifier,
2634            #[cfg(feature = "rustls")]
2635            private_ca_pem: self.private_ca_pem,
2636            #[cfg(feature = "scitt")]
2637            scitt_config: self.scitt_config,
2638            #[cfg(feature = "scitt")]
2639            scitt_key_store: self.scitt_key_store,
2640            #[cfg(feature = "scitt")]
2641            scitt_verification_cache: self.scitt_verification_cache,
2642        })
2643    }
2644}
2645
2646#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2647#[cfg(test)]
2648mod tests {
2649    use super::*;
2650    use crate::dns::MockDnsResolver;
2651    use crate::tlog::MockTransparencyLogClient;
2652    use chrono::Utc;
2653    use uuid::Uuid;
2654
2655    // Compile-time thread safety proof — fails compilation if any field
2656    // breaks Send/Sync (e.g., Rc, RefCell added to a struct).
2657    const fn _assert_send_sync<T: Send + Sync>() {}
2658    const _: () = _assert_send_sync::<ServerVerifier>();
2659    const _: () = _assert_send_sync::<ClientVerifier>();
2660    const _: () = _assert_send_sync::<AnsVerifier>();
2661    const _: () = _assert_send_sync::<BadgeCache>();
2662
2663    fn create_test_badge(host: &str, version: &str, server_fp: &str, identity_fp: &str) -> Badge {
2664        serde_json::from_value(serde_json::json!({
2665            "status": "ACTIVE",
2666            "schemaVersion": "V1",
2667            "payload": {
2668                "logId": Uuid::new_v4().to_string(),
2669                "producer": {
2670                    "event": {
2671                        "ansId": Uuid::new_v4().to_string(),
2672                        "ansName": format!("ans://{version}.{host}"),
2673                        "eventType": "AGENT_REGISTERED",
2674                        "agent": { "host": host, "name": "Test Agent", "version": version },
2675                        "attestations": {
2676                            "domainValidation": "ACME-DNS-01",
2677                            "identityCert": { "fingerprint": identity_fp, "type": "X509-OV-CLIENT" },
2678                            "serverCert": { "fingerprint": server_fp, "type": "X509-DV-SERVER" }
2679                        },
2680                        "expiresAt": (Utc::now() + chrono::Duration::days(365)).to_rfc3339(),
2681                        "issuedAt": Utc::now().to_rfc3339(),
2682                        "raId": "test-ra",
2683                        "timestamp": Utc::now().to_rfc3339()
2684                    },
2685                    "keyId": "test-key",
2686                    "signature": "test-sig"
2687                }
2688            }
2689        })).expect("test badge JSON should be valid")
2690    }
2691
2692    fn create_test_cert_identity(cn: &str, fingerprint: &str) -> CertIdentity {
2693        CertIdentity {
2694            common_name: Some(cn.to_string()),
2695            dns_sans: vec![cn.to_string()],
2696            uri_sans: vec![],
2697            fingerprint: CertFingerprint::parse(fingerprint).unwrap(),
2698        }
2699    }
2700
2701    #[tokio::test]
2702    async fn test_server_verification_success() {
2703        let host = "test.example.com";
2704        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2705
2706        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2707        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2708
2709        let dns_record = BadgeRecord {
2710            format_version: "ans-badge1".to_string(),
2711            version: Some(Version::new(1, 0, 0)),
2712            url: badge_url.to_string(),
2713        };
2714
2715        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2716
2717        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2718
2719        let verifier = ServerVerifier {
2720            dns_resolver,
2721            tlog_client,
2722            cache: None,
2723            failure_policy: FailurePolicy::FailClosed,
2724            dane_policy: DanePolicy::Disabled,
2725            dane_port: 443,
2726            trusted_ra_domains: None,
2727        };
2728
2729        let cert = create_test_cert_identity(host, fingerprint);
2730        let fqdn = Fqdn::new(host).unwrap();
2731
2732        let outcome = verifier.verify(&fqdn, &cert).await;
2733        assert!(outcome.is_success());
2734    }
2735
2736    #[tokio::test]
2737    async fn test_server_verification_not_ans_agent() {
2738        let dns_resolver = Arc::new(MockDnsResolver::new());
2739        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2740
2741        let verifier = ServerVerifier {
2742            dns_resolver,
2743            tlog_client,
2744            cache: None,
2745            failure_policy: FailurePolicy::FailClosed,
2746            dane_policy: DanePolicy::Disabled,
2747            dane_port: 443,
2748            trusted_ra_domains: None,
2749        };
2750
2751        let cert = create_test_cert_identity(
2752            "unknown.example.com",
2753            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2754        );
2755        let fqdn = Fqdn::new("unknown.example.com").unwrap();
2756
2757        let outcome = verifier.verify(&fqdn, &cert).await;
2758        assert!(outcome.is_not_ans_agent());
2759    }
2760
2761    #[tokio::test]
2762    async fn test_server_verification_fingerprint_mismatch() {
2763        let host = "test.example.com";
2764        let badge_fingerprint =
2765            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2766        let cert_fingerprint =
2767            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
2768
2769        let badge = create_test_badge(host, "v1.0.0", badge_fingerprint, "SHA256:aaa");
2770        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2771
2772        let dns_record = BadgeRecord {
2773            format_version: "ans-badge1".to_string(),
2774            version: Some(Version::new(1, 0, 0)),
2775            url: badge_url.to_string(),
2776        };
2777
2778        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2779
2780        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2781
2782        let verifier = ServerVerifier {
2783            dns_resolver,
2784            tlog_client,
2785            cache: None,
2786            failure_policy: FailurePolicy::FailClosed,
2787            dane_policy: DanePolicy::Disabled,
2788            dane_port: 443,
2789            trusted_ra_domains: None,
2790        };
2791
2792        let cert = create_test_cert_identity(host, cert_fingerprint);
2793        let fqdn = Fqdn::new(host).unwrap();
2794
2795        let outcome = verifier.verify(&fqdn, &cert).await;
2796        assert!(matches!(
2797            outcome,
2798            VerificationOutcome::FingerprintMismatch { .. }
2799        ));
2800    }
2801
2802    #[tokio::test]
2803    async fn test_server_verification_invalid_status() {
2804        let host = "test.example.com";
2805        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2806
2807        let mut badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2808        badge.status = BadgeStatus::Revoked;
2809
2810        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2811
2812        let dns_record = BadgeRecord {
2813            format_version: "ans-badge1".to_string(),
2814            version: Some(Version::new(1, 0, 0)),
2815            url: badge_url.to_string(),
2816        };
2817
2818        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2819
2820        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2821
2822        let verifier = ServerVerifier {
2823            dns_resolver,
2824            tlog_client,
2825            cache: None,
2826            failure_policy: FailurePolicy::FailClosed,
2827            dane_policy: DanePolicy::Disabled,
2828            dane_port: 443,
2829            trusted_ra_domains: None,
2830        };
2831
2832        let cert = create_test_cert_identity(host, fingerprint);
2833        let fqdn = Fqdn::new(host).unwrap();
2834
2835        let outcome = verifier.verify(&fqdn, &cert).await;
2836        assert!(matches!(
2837            outcome,
2838            VerificationOutcome::InvalidStatus {
2839                status: BadgeStatus::Revoked,
2840                ..
2841            }
2842        ));
2843    }
2844
2845    #[tokio::test]
2846    async fn test_verification_outcome_is_success() {
2847        let badge = create_test_badge(
2848            "test.example.com",
2849            "v1.0.0",
2850            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2851            "SHA256:aaa",
2852        );
2853
2854        let outcome = VerificationOutcome::Verified {
2855            badge,
2856            matched_fingerprint: CertFingerprint::parse(
2857                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2858            )
2859            .unwrap(),
2860        };
2861
2862        assert!(outcome.is_success());
2863        assert!(!outcome.is_not_ans_agent());
2864    }
2865
2866    #[tokio::test]
2867    async fn test_verification_with_cache() {
2868        let host = "test.example.com";
2869        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2870
2871        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
2872        let cache = Arc::new(BadgeCache::with_defaults());
2873        let fqdn = Fqdn::new(host).unwrap();
2874
2875        // Pre-populate cache
2876        cache
2877            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), badge)
2878            .await;
2879
2880        // Create verifier with empty DNS/TLog (should use cache)
2881        let dns_resolver = Arc::new(MockDnsResolver::new());
2882        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2883
2884        let verifier = ServerVerifier {
2885            dns_resolver,
2886            tlog_client,
2887            cache: Some(cache),
2888            failure_policy: FailurePolicy::FailClosed,
2889            dane_policy: DanePolicy::Disabled,
2890            dane_port: 443,
2891            trusted_ra_domains: None,
2892        };
2893
2894        let cert = create_test_cert_identity(host, fingerprint);
2895
2896        let outcome = verifier.verify(&fqdn, &cert).await;
2897        assert!(outcome.is_success());
2898    }
2899
2900    #[test]
2901    fn test_cert_identity_from_components() {
2902        let fingerprint = CertFingerprint::parse(
2903            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2904        )
2905        .unwrap();
2906
2907        let identity = CertIdentity::new(
2908            Some("test.example.com".to_string()),
2909            vec!["test.example.com".to_string()],
2910            vec!["ans://v1.0.0.test.example.com".to_string()],
2911            fingerprint,
2912        );
2913
2914        assert_eq!(identity.fqdn(), Some("test.example.com"));
2915        assert!(identity.ans_name().is_some());
2916        assert_eq!(identity.version(), Some(Version::new(1, 0, 0)));
2917    }
2918
2919    #[test]
2920    fn test_cert_identity_from_fingerprint_and_cn() {
2921        let fingerprint = CertFingerprint::parse(
2922            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2923        )
2924        .unwrap();
2925
2926        let identity =
2927            CertIdentity::from_fingerprint_and_cn(fingerprint, "test.example.com".to_string());
2928
2929        assert_eq!(identity.fqdn(), Some("test.example.com"));
2930        assert!(identity.ans_name().is_none()); // No URI SANs
2931    }
2932
2933    // =========================================================================
2934    // ClientVerifier Tests
2935    // =========================================================================
2936
2937    fn create_mtls_cert_identity(host: &str, version: &str, fingerprint: &str) -> CertIdentity {
2938        CertIdentity {
2939            common_name: Some(host.to_string()),
2940            dns_sans: vec![host.to_string()],
2941            uri_sans: vec![format!("ans://{}.{}", version, host)],
2942            fingerprint: CertFingerprint::parse(fingerprint).unwrap(),
2943        }
2944    }
2945
2946    #[tokio::test]
2947    async fn test_client_verification_success() {
2948        let host = "test.example.com";
2949        let version = "v1.0.0";
2950        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
2951        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
2952
2953        let badge = create_test_badge(host, version, server_fp, identity_fp);
2954        let badge_url = "https://tlog.example.com/v1/agents/test-id";
2955
2956        let dns_record = BadgeRecord {
2957            format_version: "ans-badge1".to_string(),
2958            version: Some(Version::new(1, 0, 0)),
2959            url: badge_url.to_string(),
2960        };
2961
2962        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
2963        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
2964
2965        let verifier = ClientVerifier {
2966            dns_resolver,
2967            tlog_client,
2968            cache: None,
2969            failure_policy: FailurePolicy::FailClosed,
2970            trusted_ra_domains: None,
2971        };
2972
2973        let cert = create_mtls_cert_identity(host, version, identity_fp);
2974        let outcome = verifier.verify(&cert).await;
2975
2976        assert!(outcome.is_success(), "Expected success, got: {:?}", outcome);
2977    }
2978
2979    #[tokio::test]
2980    async fn test_client_verification_no_fqdn() {
2981        let dns_resolver = Arc::new(MockDnsResolver::new());
2982        let tlog_client = Arc::new(MockTransparencyLogClient::new());
2983
2984        let verifier = ClientVerifier {
2985            dns_resolver,
2986            tlog_client,
2987            cache: None,
2988            failure_policy: FailurePolicy::FailClosed,
2989            trusted_ra_domains: None,
2990        };
2991
2992        // Create cert with no CN or DNS SANs
2993        let cert = CertIdentity {
2994            common_name: None,
2995            dns_sans: vec![],
2996            uri_sans: vec!["ans://v1.0.0.test.example.com".to_string()],
2997            fingerprint: CertFingerprint::parse(
2998                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
2999            )
3000            .unwrap(),
3001        };
3002
3003        let outcome = verifier.verify(&cert).await;
3004        assert!(matches!(outcome, VerificationOutcome::CertError(_)));
3005    }
3006
3007    #[tokio::test]
3008    async fn test_client_verification_no_ans_name() {
3009        let dns_resolver = Arc::new(MockDnsResolver::new());
3010        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3011
3012        let verifier = ClientVerifier {
3013            dns_resolver,
3014            tlog_client,
3015            cache: None,
3016            failure_policy: FailurePolicy::FailClosed,
3017            trusted_ra_domains: None,
3018        };
3019
3020        // Create cert with CN but no URI SANs
3021        let cert = CertIdentity {
3022            common_name: Some("test.example.com".to_string()),
3023            dns_sans: vec!["test.example.com".to_string()],
3024            uri_sans: vec![],
3025            fingerprint: CertFingerprint::parse(
3026                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3027            )
3028            .unwrap(),
3029        };
3030
3031        let outcome = verifier.verify(&cert).await;
3032        assert!(matches!(outcome, VerificationOutcome::CertError(_)));
3033    }
3034
3035    #[tokio::test]
3036    async fn test_client_verification_fingerprint_mismatch() {
3037        let host = "test.example.com";
3038        let version = "v1.0.0";
3039        let badge_identity_fp =
3040            "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
3041        let cert_identity_fp =
3042            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
3043
3044        let badge = create_test_badge(host, version, "SHA256:server", badge_identity_fp);
3045        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3046
3047        let dns_record = BadgeRecord {
3048            format_version: "ans-badge1".to_string(),
3049            version: Some(Version::new(1, 0, 0)),
3050            url: badge_url.to_string(),
3051        };
3052
3053        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3054        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3055
3056        let verifier = ClientVerifier {
3057            dns_resolver,
3058            tlog_client,
3059            cache: None,
3060            failure_policy: FailurePolicy::FailClosed,
3061            trusted_ra_domains: None,
3062        };
3063
3064        let cert = create_mtls_cert_identity(host, version, cert_identity_fp);
3065        let outcome = verifier.verify(&cert).await;
3066
3067        assert!(matches!(
3068            outcome,
3069            VerificationOutcome::FingerprintMismatch { .. }
3070        ));
3071    }
3072
3073    #[tokio::test]
3074    async fn test_client_verification_ans_name_mismatch() {
3075        let host = "test.example.com";
3076        let badge_version = "v1.0.0";
3077        let cert_version = "v2.0.0";
3078        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
3079
3080        // Badge has v1.0.0, cert has v2.0.0
3081        let badge = create_test_badge(host, badge_version, "SHA256:server", identity_fp);
3082        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3083
3084        let dns_record = BadgeRecord {
3085            format_version: "ans-badge1".to_string(),
3086            version: Some(Version::new(2, 0, 0)),
3087            url: badge_url.to_string(),
3088        };
3089
3090        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3091        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3092
3093        let verifier = ClientVerifier {
3094            dns_resolver,
3095            tlog_client,
3096            cache: None,
3097            failure_policy: FailurePolicy::FailClosed,
3098            trusted_ra_domains: None,
3099        };
3100
3101        let cert = create_mtls_cert_identity(host, cert_version, identity_fp);
3102        let outcome = verifier.verify(&cert).await;
3103
3104        assert!(matches!(
3105            outcome,
3106            VerificationOutcome::AnsNameMismatch { .. }
3107        ));
3108    }
3109
3110    // =========================================================================
3111    // VerificationOutcome Tests
3112    // =========================================================================
3113
3114    #[test]
3115    fn test_verification_outcome_badge() {
3116        let badge = create_test_badge(
3117            "test.example.com",
3118            "v1.0.0",
3119            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3120            "SHA256:aaa",
3121        );
3122
3123        // Verified has badge
3124        let outcome = VerificationOutcome::Verified {
3125            badge: badge.clone(),
3126            matched_fingerprint: CertFingerprint::parse(
3127                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3128            )
3129            .unwrap(),
3130        };
3131        assert!(outcome.badge().is_some());
3132
3133        // InvalidStatus has badge
3134        let outcome = VerificationOutcome::InvalidStatus {
3135            status: BadgeStatus::Revoked,
3136            badge: badge.clone(),
3137        };
3138        assert!(outcome.badge().is_some());
3139
3140        // FingerprintMismatch has badge
3141        let outcome = VerificationOutcome::FingerprintMismatch {
3142            expected: "SHA256:a".to_string(),
3143            actual: "SHA256:b".to_string(),
3144            badge: badge.clone(),
3145        };
3146        assert!(outcome.badge().is_some());
3147
3148        // HostnameMismatch has badge
3149        let outcome = VerificationOutcome::HostnameMismatch {
3150            expected: "a.com".to_string(),
3151            actual: "b.com".to_string(),
3152            badge: badge.clone(),
3153        };
3154        assert!(outcome.badge().is_some());
3155
3156        // AnsNameMismatch has badge
3157        let outcome = VerificationOutcome::AnsNameMismatch {
3158            expected: "ans://v1.0.0.a.com".to_string(),
3159            actual: "ans://v2.0.0.a.com".to_string(),
3160            badge,
3161        };
3162        assert!(outcome.badge().is_some());
3163
3164        // NotAnsAgent has no badge
3165        let outcome = VerificationOutcome::NotAnsAgent {
3166            fqdn: "test.com".to_string(),
3167        };
3168        assert!(outcome.badge().is_none());
3169
3170        // DnsError has no badge
3171        let outcome = VerificationOutcome::DnsError(DnsError::NotFound {
3172            fqdn: "test.com".to_string(),
3173        });
3174        assert!(outcome.badge().is_none());
3175    }
3176
3177    #[test]
3178    fn test_verification_outcome_into_result() {
3179        let badge = create_test_badge(
3180            "test.example.com",
3181            "v1.0.0",
3182            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3183            "SHA256:aaa",
3184        );
3185
3186        // Verified -> Ok
3187        let outcome = VerificationOutcome::Verified {
3188            badge: badge.clone(),
3189            matched_fingerprint: CertFingerprint::parse(
3190                "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3191            )
3192            .unwrap(),
3193        };
3194        assert!(outcome.into_result().is_ok());
3195
3196        // NotAnsAgent -> Err
3197        let outcome = VerificationOutcome::NotAnsAgent {
3198            fqdn: "test.com".to_string(),
3199        };
3200        assert!(outcome.into_result().is_err());
3201
3202        // InvalidStatus -> Err
3203        let outcome = VerificationOutcome::InvalidStatus {
3204            status: BadgeStatus::Revoked,
3205            badge: badge.clone(),
3206        };
3207        assert!(outcome.into_result().is_err());
3208
3209        // FingerprintMismatch -> Err
3210        let outcome = VerificationOutcome::FingerprintMismatch {
3211            expected: "a".to_string(),
3212            actual: "b".to_string(),
3213            badge: badge.clone(),
3214        };
3215        assert!(outcome.into_result().is_err());
3216
3217        // HostnameMismatch -> Err
3218        let outcome = VerificationOutcome::HostnameMismatch {
3219            expected: "a.com".to_string(),
3220            actual: "b.com".to_string(),
3221            badge: badge.clone(),
3222        };
3223        assert!(outcome.into_result().is_err());
3224
3225        // AnsNameMismatch -> Err
3226        let outcome = VerificationOutcome::AnsNameMismatch {
3227            expected: "a".to_string(),
3228            actual: "b".to_string(),
3229            badge,
3230        };
3231        assert!(outcome.into_result().is_err());
3232
3233        // DnsError -> Err
3234        let outcome = VerificationOutcome::DnsError(DnsError::NotFound {
3235            fqdn: "test.com".to_string(),
3236        });
3237        assert!(outcome.into_result().is_err());
3238
3239        // TlogError -> Err
3240        let outcome = VerificationOutcome::TlogError(TlogError::ServiceUnavailable);
3241        assert!(outcome.into_result().is_err());
3242
3243        // DaneError -> Err
3244        let outcome = VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
3245        assert!(outcome.into_result().is_err());
3246    }
3247
3248    // =========================================================================
3249    // Hostname Mismatch Tests
3250    // =========================================================================
3251
3252    #[tokio::test]
3253    async fn test_server_verification_hostname_mismatch() {
3254        let badge_host = "badge.example.com";
3255        let cert_host = "different.example.com";
3256        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3257
3258        let badge = create_test_badge(badge_host, "v1.0.0", fingerprint, "SHA256:aaa");
3259        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3260
3261        let dns_record = BadgeRecord {
3262            format_version: "ans-badge1".to_string(),
3263            version: Some(Version::new(1, 0, 0)),
3264            url: badge_url.to_string(),
3265        };
3266
3267        // DNS lookup uses cert_host but badge contains badge_host
3268        let dns_resolver =
3269            Arc::new(MockDnsResolver::new().with_records(cert_host, vec![dns_record]));
3270        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3271
3272        let verifier = ServerVerifier {
3273            dns_resolver,
3274            tlog_client,
3275            cache: None,
3276            failure_policy: FailurePolicy::FailClosed,
3277            dane_policy: DanePolicy::Disabled,
3278            dane_port: 443,
3279            trusted_ra_domains: None,
3280        };
3281
3282        let cert = create_test_cert_identity(cert_host, fingerprint);
3283        let fqdn = Fqdn::new(cert_host).unwrap();
3284
3285        let outcome = verifier.verify(&fqdn, &cert).await;
3286        assert!(
3287            matches!(outcome, VerificationOutcome::HostnameMismatch { .. }),
3288            "Expected HostnameMismatch, got: {:?}",
3289            outcome
3290        );
3291    }
3292
3293    // =========================================================================
3294    // Prefetch Tests
3295    // =========================================================================
3296
3297    #[tokio::test]
3298    async fn test_server_verifier_prefetch_success() {
3299        let host = "test.example.com";
3300        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3301
3302        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3303        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3304
3305        let dns_record = BadgeRecord {
3306            format_version: "ans-badge1".to_string(),
3307            version: Some(Version::new(1, 0, 0)),
3308            url: badge_url.to_string(),
3309        };
3310
3311        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3312        let tlog_client =
3313            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge.clone()));
3314
3315        let verifier = ServerVerifier {
3316            dns_resolver,
3317            tlog_client,
3318            cache: Some(Arc::new(BadgeCache::with_defaults())),
3319            failure_policy: FailurePolicy::FailClosed,
3320            dane_policy: DanePolicy::Disabled,
3321            dane_port: 443,
3322            trusted_ra_domains: None,
3323        };
3324
3325        let fqdn = Fqdn::new(host).unwrap();
3326        let result = verifier.prefetch(&fqdn).await;
3327
3328        assert!(result.is_ok());
3329        assert_eq!(result.unwrap().agent_host(), host);
3330    }
3331
3332    #[tokio::test]
3333    async fn test_server_verifier_prefetch_not_found() {
3334        let dns_resolver = Arc::new(MockDnsResolver::new());
3335        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3336
3337        let verifier = ServerVerifier {
3338            dns_resolver,
3339            tlog_client,
3340            cache: None,
3341            failure_policy: FailurePolicy::FailClosed,
3342            dane_policy: DanePolicy::Disabled,
3343            dane_port: 443,
3344            trusted_ra_domains: None,
3345        };
3346
3347        let fqdn = Fqdn::new("unknown.example.com").unwrap();
3348        let result = verifier.prefetch(&fqdn).await;
3349
3350        assert!(result.is_err());
3351        assert!(matches!(result.unwrap_err(), AnsError::Dns(_)));
3352    }
3353
3354    // =========================================================================
3355    // FailurePolicy Tests
3356    // =========================================================================
3357
3358    #[tokio::test]
3359    async fn test_failure_policy_fail_open_with_cache_no_cache() {
3360        let dns_resolver = Arc::new(MockDnsResolver::new().with_error(
3361            "test.example.com",
3362            DnsError::LookupFailed {
3363                fqdn: "test.example.com".to_string(),
3364                reason: "timeout".to_string(),
3365            },
3366        ));
3367        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3368
3369        let verifier = ServerVerifier {
3370            dns_resolver,
3371            tlog_client,
3372            cache: Some(Arc::new(BadgeCache::with_defaults())),
3373            failure_policy: FailurePolicy::FailOpenWithCache {
3374                max_staleness: Duration::from_secs(600),
3375            },
3376            dane_policy: DanePolicy::Disabled,
3377            dane_port: 443,
3378            trusted_ra_domains: None,
3379        };
3380
3381        let cert = create_test_cert_identity(
3382            "test.example.com",
3383            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
3384        );
3385        let fqdn = Fqdn::new("test.example.com").unwrap();
3386
3387        let outcome = verifier.verify(&fqdn, &cert).await;
3388        // No cached badge, so returns DNS error
3389        assert!(matches!(outcome, VerificationOutcome::DnsError(_)));
3390    }
3391
3392    #[tokio::test]
3393    async fn test_failure_policy_fail_open_with_cache_uses_cache() {
3394        let host = "test.example.com";
3395        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3396
3397        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3398        let cache = Arc::new(BadgeCache::with_defaults());
3399        let fqdn = Fqdn::new(host).unwrap();
3400
3401        // Pre-populate cache
3402        cache
3403            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), badge)
3404            .await;
3405
3406        let dns_resolver = Arc::new(MockDnsResolver::new().with_error(
3407            host,
3408            DnsError::LookupFailed {
3409                fqdn: host.to_string(),
3410                reason: "timeout".to_string(),
3411            },
3412        ));
3413        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3414
3415        let verifier = ServerVerifier {
3416            dns_resolver,
3417            tlog_client,
3418            cache: Some(cache),
3419            failure_policy: FailurePolicy::FailOpenWithCache {
3420                max_staleness: Duration::from_secs(600),
3421            },
3422            dane_policy: DanePolicy::Disabled,
3423            dane_port: 443,
3424            trusted_ra_domains: None,
3425        };
3426
3427        let cert = create_test_cert_identity(host, fingerprint);
3428
3429        let outcome = verifier.verify(&fqdn, &cert).await;
3430        // Should use cached badge and verify successfully
3431        assert!(
3432            outcome.is_success(),
3433            "Expected success with cache, got: {:?}",
3434            outcome
3435        );
3436    }
3437
3438    #[test]
3439    fn test_cert_identity_from_der_server_cert() {
3440        use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType};
3441
3442        // Generate a server certificate on the fly
3443        let key_pair = KeyPair::generate().unwrap();
3444        let mut params = CertificateParams::default();
3445        params
3446            .distinguished_name
3447            .push(DnType::CommonName, "test.agent.local");
3448        params.subject_alt_names.push(SanType::DnsName(
3449            "test.agent.local".to_string().try_into().unwrap(),
3450        ));
3451        params.subject_alt_names.push(SanType::URI(
3452            "ans://v1.0.0.test.agent.local".try_into().unwrap(),
3453        ));
3454        params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
3455
3456        let cert = params.self_signed(&key_pair).unwrap();
3457        let der = cert.der();
3458
3459        let identity = CertIdentity::from_der(der).expect("should parse DER certificate");
3460
3461        // Verify CN
3462        assert_eq!(
3463            identity.common_name.as_deref(),
3464            Some("test.agent.local"),
3465            "CN should be test.agent.local"
3466        );
3467
3468        // Verify DNS SAN
3469        assert!(
3470            identity.dns_sans.contains(&"test.agent.local".to_string()),
3471            "DNS SANs should contain test.agent.local, got: {:?}",
3472            identity.dns_sans
3473        );
3474
3475        // Verify URI SAN (ANS name)
3476        assert!(
3477            identity
3478                .uri_sans
3479                .contains(&"ans://v1.0.0.test.agent.local".to_string()),
3480            "URI SANs should contain ans://v1.0.0.test.agent.local, got: {:?}",
3481            identity.uri_sans
3482        );
3483
3484        // Verify fingerprint matches what we compute from the same DER bytes
3485        let expected_fp = CertFingerprint::from_der(der);
3486        assert_eq!(
3487            identity.fingerprint, expected_fp,
3488            "Fingerprint should match computed fingerprint from same DER"
3489        );
3490
3491        // Verify convenience methods
3492        assert_eq!(identity.fqdn(), Some("test.agent.local"));
3493        let ans_name = identity.ans_name().expect("should have ANS name");
3494        assert_eq!(ans_name.fqdn().as_str(), "test.agent.local");
3495        assert_eq!(identity.version(), Some(Version::new(1, 0, 0)));
3496    }
3497
3498    #[test]
3499    fn test_cert_identity_from_der_client_cert() {
3500        use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, SanType};
3501
3502        // Generate a client (identity) certificate on the fly
3503        let key_pair = KeyPair::generate().unwrap();
3504        let mut params = CertificateParams::default();
3505        params
3506            .distinguished_name
3507            .push(DnType::CommonName, "test.agent.local");
3508        params.subject_alt_names.push(SanType::DnsName(
3509            "test.agent.local".to_string().try_into().unwrap(),
3510        ));
3511        params.subject_alt_names.push(SanType::URI(
3512            "ans://v1.0.0.test.agent.local".try_into().unwrap(),
3513        ));
3514        params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
3515
3516        let cert = params.self_signed(&key_pair).unwrap();
3517        let der = cert.der();
3518
3519        let identity = CertIdentity::from_der(der).expect("should parse DER certificate");
3520
3521        assert_eq!(identity.common_name.as_deref(), Some("test.agent.local"));
3522        assert!(identity.dns_sans.contains(&"test.agent.local".to_string()));
3523        assert!(
3524            identity
3525                .uri_sans
3526                .contains(&"ans://v1.0.0.test.agent.local".to_string())
3527        );
3528
3529        let expected_fp = CertFingerprint::from_der(der);
3530        assert_eq!(identity.fingerprint, expected_fp);
3531    }
3532
3533    #[test]
3534    fn test_cert_identity_from_der_invalid_bytes() {
3535        let result = CertIdentity::from_der(b"not a certificate");
3536        assert!(result.is_err(), "Should fail on invalid DER bytes");
3537    }
3538
3539    #[tokio::test]
3540    async fn test_server_verifier_builder_dane_policy() {
3541        let dns = Arc::new(MockDnsResolver::new());
3542        let tlog = Arc::new(MockTransparencyLogClient::new());
3543
3544        // with_dane_if_present convenience method
3545        let verifier = ServerVerifier::builder()
3546            .dns_resolver(dns.clone())
3547            .tlog_client(tlog.clone())
3548            .with_dane_if_present()
3549            .build()
3550            .await
3551            .unwrap();
3552        assert_eq!(verifier.dane_policy, DanePolicy::ValidateIfPresent);
3553
3554        // require_dane convenience method
3555        let verifier = ServerVerifier::builder()
3556            .dns_resolver(dns.clone())
3557            .tlog_client(tlog.clone())
3558            .require_dane()
3559            .build()
3560            .await
3561            .unwrap();
3562        assert_eq!(verifier.dane_policy, DanePolicy::Required);
3563
3564        // explicit dane_policy
3565        let verifier = ServerVerifier::builder()
3566            .dns_resolver(dns.clone())
3567            .tlog_client(tlog.clone())
3568            .dane_policy(DanePolicy::Disabled)
3569            .build()
3570            .await
3571            .unwrap();
3572        assert_eq!(verifier.dane_policy, DanePolicy::Disabled);
3573    }
3574
3575    #[tokio::test]
3576    async fn test_server_verifier_builder_dane_port() {
3577        let dns = Arc::new(MockDnsResolver::new());
3578        let tlog = Arc::new(MockTransparencyLogClient::new());
3579
3580        // Default port is 443
3581        let verifier = ServerVerifier::builder()
3582            .dns_resolver(dns.clone())
3583            .tlog_client(tlog.clone())
3584            .build()
3585            .await
3586            .unwrap();
3587        assert_eq!(verifier.dane_port, 443);
3588
3589        // Custom port
3590        let verifier = ServerVerifier::builder()
3591            .dns_resolver(dns.clone())
3592            .tlog_client(tlog.clone())
3593            .dane_port(8443)
3594            .build()
3595            .await
3596            .unwrap();
3597        assert_eq!(verifier.dane_port, 8443);
3598    }
3599
3600    #[tokio::test]
3601    async fn test_server_verifier_builder_failure_policy() {
3602        let dns = Arc::new(MockDnsResolver::new());
3603        let tlog = Arc::new(MockTransparencyLogClient::new());
3604
3605        let verifier = ServerVerifier::builder()
3606            .dns_resolver(dns)
3607            .tlog_client(tlog)
3608            .failure_policy(FailurePolicy::FailClosed)
3609            .build()
3610            .await
3611            .unwrap();
3612        assert!(matches!(verifier.failure_policy, FailurePolicy::FailClosed));
3613    }
3614
3615    // =========================================================================
3616    // Refresh-on-Mismatch Tests
3617    // =========================================================================
3618
3619    #[tokio::test]
3620    async fn test_server_verification_refresh_on_mismatch_succeeds() {
3621        let host = "test.example.com";
3622        let old_fp = "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
3623        let new_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3624
3625        // First badge has old fingerprint (will mismatch)
3626        // After refresh, tlog returns badge with new fingerprint
3627        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3628        let updated_badge = create_test_badge(host, "v1.0.0", new_fp, "SHA256:aaa");
3629
3630        let dns_record = BadgeRecord {
3631            format_version: "ans-badge1".to_string(),
3632            version: Some(Version::new(1, 0, 0)),
3633            url: badge_url.to_string(),
3634        };
3635
3636        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3637        // Mock always returns updated badge (simulates tlog updated after cert renewal)
3638        let tlog_client =
3639            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, updated_badge));
3640
3641        let cache = Arc::new(BadgeCache::with_defaults());
3642        let fqdn = Fqdn::new(host).unwrap();
3643
3644        // Pre-populate cache with stale badge (old fingerprint)
3645        let stale_badge = create_test_badge(host, "v1.0.0", old_fp, "SHA256:aaa");
3646        cache
3647            .insert_for_fqdn_version(&fqdn, &Version::new(1, 0, 0), stale_badge)
3648            .await;
3649
3650        let verifier = ServerVerifier {
3651            dns_resolver,
3652            tlog_client,
3653            cache: Some(cache),
3654            failure_policy: FailurePolicy::FailClosed,
3655            dane_policy: DanePolicy::Disabled,
3656            dane_port: 443,
3657            trusted_ra_domains: None,
3658        };
3659
3660        // Cert has the NEW fingerprint — cache has OLD → mismatch → refresh → success
3661        let cert = create_test_cert_identity(host, new_fp);
3662        let outcome = verifier.verify(&fqdn, &cert).await;
3663        assert!(
3664            outcome.is_success(),
3665            "Expected success after refresh, got: {:?}",
3666            outcome
3667        );
3668    }
3669
3670    #[tokio::test]
3671    async fn test_server_verification_refresh_on_mismatch_still_fails() {
3672        let host = "test.example.com";
3673        let badge_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3674        let cert_fp = "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
3675
3676        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3677        // Badge always has badge_fp, cert always has cert_fp — never matches
3678        let badge = create_test_badge(host, "v1.0.0", badge_fp, "SHA256:aaa");
3679
3680        let dns_record = BadgeRecord {
3681            format_version: "ans-badge1".to_string(),
3682            version: Some(Version::new(1, 0, 0)),
3683            url: badge_url.to_string(),
3684        };
3685
3686        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3687        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3688
3689        let verifier = ServerVerifier {
3690            dns_resolver,
3691            tlog_client,
3692            cache: None,
3693            failure_policy: FailurePolicy::FailClosed,
3694            dane_policy: DanePolicy::Disabled,
3695            dane_port: 443,
3696            trusted_ra_domains: None,
3697        };
3698
3699        let cert = create_test_cert_identity(host, cert_fp);
3700        let fqdn = Fqdn::new(host).unwrap();
3701
3702        let outcome = verifier.verify(&fqdn, &cert).await;
3703        assert!(
3704            matches!(outcome, VerificationOutcome::FingerprintMismatch { .. }),
3705            "Expected FingerprintMismatch after refresh still fails, got: {:?}",
3706            outcome
3707        );
3708    }
3709
3710    #[tokio::test]
3711    async fn test_client_verification_refresh_on_mismatch_succeeds() {
3712        let host = "test.example.com";
3713        let version = "v1.0.0";
3714        let old_identity_fp =
3715            "SHA256:0000000000000000000000000000000000000000000000000000000000000000";
3716        let new_identity_fp =
3717            "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
3718        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3719
3720        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3721        // Tlog returns updated badge with new identity fingerprint
3722        let updated_badge = create_test_badge(host, version, server_fp, new_identity_fp);
3723
3724        let dns_record = BadgeRecord {
3725            format_version: "ans-badge1".to_string(),
3726            version: Some(Version::new(1, 0, 0)),
3727            url: badge_url.to_string(),
3728        };
3729
3730        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3731        let tlog_client =
3732            Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, updated_badge));
3733
3734        let cache = Arc::new(BadgeCache::with_defaults());
3735        let fqdn = Fqdn::new(host).unwrap();
3736        let ver = Version::new(1, 0, 0);
3737
3738        // Pre-populate cache with stale badge (old identity fingerprint)
3739        let stale_badge = create_test_badge(host, version, server_fp, old_identity_fp);
3740        cache
3741            .insert_for_fqdn_version(&fqdn, &ver, stale_badge)
3742            .await;
3743
3744        let verifier = ClientVerifier {
3745            dns_resolver,
3746            tlog_client,
3747            cache: Some(cache),
3748            failure_policy: FailurePolicy::FailClosed,
3749            trusted_ra_domains: None,
3750        };
3751
3752        // Client cert has new fingerprint — cache has old → mismatch → refresh → success
3753        let cert = create_mtls_cert_identity(host, version, new_identity_fp);
3754        let outcome = verifier.verify(&cert).await;
3755        assert!(
3756            outcome.is_success(),
3757            "Expected success after client refresh, got: {:?}",
3758            outcome
3759        );
3760    }
3761
3762    // =========================================================================
3763    // Trusted RA Domain Validation Tests
3764    // =========================================================================
3765
3766    #[test]
3767    fn test_validate_badge_domain_unit_allows_when_none() {
3768        assert!(validate_badge_domain(None, "https://tlog.example.com/v1/agents/test").is_ok());
3769    }
3770
3771    #[test]
3772    fn test_validate_badge_domain_unit_allows_trusted() {
3773        let trusted: HashSet<String> = ["tlog.example.com".to_string()].into();
3774        assert!(
3775            validate_badge_domain(Some(&trusted), "https://tlog.example.com/v1/agents/test")
3776                .is_ok()
3777        );
3778    }
3779
3780    #[test]
3781    fn test_validate_badge_domain_unit_rejects_untrusted() {
3782        let trusted: HashSet<String> = ["tlog.example.com".to_string()].into();
3783        let err = validate_badge_domain(Some(&trusted), "https://evil.attacker.com/v1/agents/test")
3784            .unwrap_err();
3785        assert!(
3786            matches!(err, TlogError::UntrustedDomain { domain, .. } if domain == "evil.attacker.com")
3787        );
3788    }
3789
3790    #[test]
3791    fn test_validate_badge_domain_unit_multiple_trusted() {
3792        let trusted: HashSet<String> = [
3793            "tlog1.example.com".to_string(),
3794            "tlog2.example.com".to_string(),
3795        ]
3796        .into();
3797        assert!(validate_badge_domain(Some(&trusted), "https://tlog1.example.com/badge").is_ok());
3798        assert!(validate_badge_domain(Some(&trusted), "https://tlog2.example.com/badge").is_ok());
3799        assert!(validate_badge_domain(Some(&trusted), "https://tlog3.example.com/badge").is_err());
3800    }
3801
3802    #[tokio::test]
3803    async fn test_trusted_ra_none_allows_all() {
3804        let host = "test.example.com";
3805        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3806        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3807        let badge_url = "https://any-domain.example.com/v1/agents/test-id";
3808
3809        let dns_record = BadgeRecord {
3810            format_version: "ans-badge1".to_string(),
3811            version: Some(Version::new(1, 0, 0)),
3812            url: badge_url.to_string(),
3813        };
3814        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3815        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3816
3817        let verifier = ServerVerifier {
3818            dns_resolver,
3819            tlog_client,
3820            cache: None,
3821            failure_policy: FailurePolicy::FailClosed,
3822            dane_policy: DanePolicy::Disabled,
3823            dane_port: 443,
3824            trusted_ra_domains: None,
3825        };
3826
3827        let cert = create_test_cert_identity(host, fingerprint);
3828        let fqdn = Fqdn::new(host).unwrap();
3829        let outcome = verifier.verify(&fqdn, &cert).await;
3830        assert!(outcome.is_success(), "None should allow all domains");
3831    }
3832
3833    #[tokio::test]
3834    async fn test_trusted_ra_allows_trusted_domain() {
3835        let host = "test.example.com";
3836        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3837        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3838        let badge_url = "https://tlog.example.com/v1/agents/test-id";
3839
3840        let dns_record = BadgeRecord {
3841            format_version: "ans-badge1".to_string(),
3842            version: Some(Version::new(1, 0, 0)),
3843            url: badge_url.to_string(),
3844        };
3845        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3846        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3847
3848        let verifier = ServerVerifier {
3849            dns_resolver,
3850            tlog_client,
3851            cache: None,
3852            failure_policy: FailurePolicy::FailClosed,
3853            dane_policy: DanePolicy::Disabled,
3854            dane_port: 443,
3855            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3856        };
3857
3858        let cert = create_test_cert_identity(host, fingerprint);
3859        let fqdn = Fqdn::new(host).unwrap();
3860        let outcome = verifier.verify(&fqdn, &cert).await;
3861        assert!(outcome.is_success(), "Trusted domain should succeed");
3862    }
3863
3864    #[tokio::test]
3865    async fn test_trusted_ra_rejects_untrusted_domain() {
3866        let host = "test.example.com";
3867        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3868        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
3869        let badge_url = "https://evil.attacker.com/v1/agents/test-id";
3870
3871        let dns_record = BadgeRecord {
3872            format_version: "ans-badge1".to_string(),
3873            version: Some(Version::new(1, 0, 0)),
3874            url: badge_url.to_string(),
3875        };
3876        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3877        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3878
3879        let verifier = ServerVerifier {
3880            dns_resolver,
3881            tlog_client,
3882            cache: None,
3883            failure_policy: FailurePolicy::FailClosed,
3884            dane_policy: DanePolicy::Disabled,
3885            dane_port: 443,
3886            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3887        };
3888
3889        let cert = create_test_cert_identity(host, fingerprint);
3890        let fqdn = Fqdn::new(host).unwrap();
3891        let outcome = verifier.verify(&fqdn, &cert).await;
3892        assert!(
3893            matches!(
3894                outcome,
3895                VerificationOutcome::TlogError(TlogError::UntrustedDomain { .. })
3896            ),
3897            "Untrusted domain should be rejected, got: {:?}",
3898            outcome
3899        );
3900    }
3901
3902    #[tokio::test]
3903    async fn test_trusted_ra_client_rejects_untrusted() {
3904        let host = "test.example.com";
3905        let version = "v1.0.0";
3906        let identity_fp = "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496";
3907        let server_fp = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
3908        let badge = create_test_badge(host, version, server_fp, identity_fp);
3909        let badge_url = "https://evil.attacker.com/v1/agents/test-id";
3910
3911        let dns_record = BadgeRecord {
3912            format_version: "ans-badge1".to_string(),
3913            version: Some(Version::new(1, 0, 0)),
3914            url: badge_url.to_string(),
3915        };
3916        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
3917        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
3918
3919        let verifier = ClientVerifier {
3920            dns_resolver,
3921            tlog_client,
3922            cache: None,
3923            failure_policy: FailurePolicy::FailClosed,
3924            trusted_ra_domains: Some(["tlog.example.com".to_string()].into()),
3925        };
3926
3927        let cert = create_mtls_cert_identity(host, version, identity_fp);
3928        let outcome = verifier.verify(&cert).await;
3929        assert!(
3930            matches!(
3931                outcome,
3932                VerificationOutcome::TlogError(TlogError::UntrustedDomain { .. })
3933            ),
3934            "Client verifier should reject untrusted domain, got: {:?}",
3935            outcome
3936        );
3937    }
3938
3939    #[tokio::test]
3940    async fn test_trusted_ra_builder_propagation() {
3941        let dns_resolver = Arc::new(MockDnsResolver::new());
3942        let tlog_client = Arc::new(MockTransparencyLogClient::new());
3943
3944        let verifier = ServerVerifier::builder()
3945            .dns_resolver(dns_resolver as Arc<dyn DnsResolver>)
3946            .tlog_client(tlog_client as Arc<dyn TransparencyLogClient>)
3947            .trusted_ra_domains(["tlog.example.com", "tlog2.example.com"])
3948            .build()
3949            .await
3950            .unwrap();
3951
3952        // Verify the builder propagated the trusted domains correctly
3953        let trusted = verifier.trusted_ra_domains.as_ref().unwrap();
3954        assert!(trusted.contains("tlog.example.com"));
3955        assert!(trusted.contains("tlog2.example.com"));
3956        assert_eq!(trusted.len(), 2);
3957    }
3958
3959    // =========================================================================
3960    // 7a: VerificationOutcome::into_result() — CertError and ParseError branches
3961    // =========================================================================
3962
3963    #[test]
3964    fn test_outcome_into_result_cert_error() {
3965        let outcome =
3966            VerificationOutcome::CertError(CryptoError::ParseFailed("bad cert".to_string()));
3967        let err = outcome.into_result().unwrap_err();
3968        assert!(matches!(err, AnsError::Certificate(_)));
3969    }
3970
3971    #[test]
3972    fn test_outcome_into_result_parse_error() {
3973        let outcome = VerificationOutcome::ParseError(ans_types::ParseError::InvalidFqdn(
3974            "bad fqdn".to_string(),
3975        ));
3976        let err = outcome.into_result().unwrap_err();
3977        assert!(matches!(err, AnsError::Parse(_)));
3978    }
3979
3980    #[test]
3981    fn test_outcome_into_result_dane_error() {
3982        let outcome = VerificationOutcome::DaneError(DaneError::FingerprintMismatch);
3983        let err = outcome.into_result().unwrap_err();
3984        assert!(matches!(
3985            err,
3986            AnsError::Verification(VerificationError::DaneVerificationFailed(_))
3987        ));
3988    }
3989
3990    #[test]
3991    fn test_outcome_into_result_dns_error() {
3992        let outcome = VerificationOutcome::DnsError(DnsError::Timeout {
3993            fqdn: "test.example.com".to_string(),
3994        });
3995        let err = outcome.into_result().unwrap_err();
3996        assert!(matches!(err, AnsError::Dns(DnsError::Timeout { .. })));
3997    }
3998
3999    #[test]
4000    fn test_outcome_into_result_tlog_error() {
4001        let outcome = VerificationOutcome::TlogError(TlogError::ServiceUnavailable);
4002        let err = outcome.into_result().unwrap_err();
4003        assert!(matches!(
4004            err,
4005            AnsError::TransparencyLog(TlogError::ServiceUnavailable)
4006        ));
4007    }
4008
4009    // =========================================================================
4010    // 7b: AnsVerifierBuilder DNS presets
4011    // =========================================================================
4012
4013    #[tokio::test]
4014    async fn test_builder_dns_cloudflare() {
4015        let dns = Arc::new(MockDnsResolver::new());
4016        let tlog = Arc::new(MockTransparencyLogClient::new());
4017
4018        // Test that the builder method configures correctly
4019        let verifier = AnsVerifier::builder()
4020            .dns_resolver(dns as Arc<dyn DnsResolver>)
4021            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4022            .dns_cloudflare() // preset is ignored when custom resolver is set
4023            .build()
4024            .await
4025            .unwrap();
4026
4027        // Verify the builder produces a working verifier
4028        let dbg = format!("{verifier:?}");
4029        assert!(dbg.contains("AnsVerifier"));
4030    }
4031
4032    #[tokio::test]
4033    async fn test_builder_dns_nameservers() {
4034        let tlog = Arc::new(MockTransparencyLogClient::new());
4035
4036        let verifier = AnsVerifier::builder()
4037            .dns_nameservers(&[std::net::Ipv4Addr::new(1, 1, 1, 1)])
4038            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4039            .build()
4040            .await
4041            .unwrap();
4042
4043        let dbg = format!("{verifier:?}");
4044        assert!(dbg.contains("AnsVerifier"));
4045    }
4046
4047    #[tokio::test]
4048    async fn test_builder_dns_preset_path() {
4049        let tlog = Arc::new(MockTransparencyLogClient::new());
4050
4051        let verifier = AnsVerifier::builder()
4052            .dns_preset(DnsResolverConfig::Cloudflare)
4053            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4054            .build()
4055            .await
4056            .unwrap();
4057
4058        let dbg = format!("{verifier:?}");
4059        assert!(dbg.contains("AnsVerifier"));
4060    }
4061
4062    // =========================================================================
4063    // 7c: AnsVerifier rustls methods
4064    // =========================================================================
4065
4066    #[cfg(feature = "rustls")]
4067    #[tokio::test]
4068    async fn test_client_cert_verifier_without_pem() {
4069        let _ = rustls::crypto::ring::default_provider().install_default();
4070        let dns = Arc::new(MockDnsResolver::new());
4071        let tlog = Arc::new(MockTransparencyLogClient::new());
4072
4073        let verifier = AnsVerifier::builder()
4074            .dns_resolver(dns as Arc<dyn DnsResolver>)
4075            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4076            .build()
4077            .await
4078            .unwrap();
4079
4080        let result = verifier.client_cert_verifier();
4081        assert!(result.is_err());
4082    }
4083
4084    #[cfg(feature = "rustls")]
4085    #[tokio::test]
4086    async fn test_client_cert_verifier_with_pem() {
4087        let _ = rustls::crypto::ring::default_provider().install_default();
4088        let ca = rcgen::generate_simple_self_signed(vec!["ANS Test CA".to_string()]).unwrap();
4089        let ca_pem = ca.cert.pem();
4090
4091        let dns = Arc::new(MockDnsResolver::new());
4092        let tlog = Arc::new(MockTransparencyLogClient::new());
4093
4094        let verifier = AnsVerifier::builder()
4095            .dns_resolver(dns as Arc<dyn DnsResolver>)
4096            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4097            .private_ca_pem(ca_pem.as_bytes().to_vec())
4098            .build()
4099            .await
4100            .unwrap();
4101
4102        let cv = verifier.client_cert_verifier().unwrap();
4103        assert!(cv.requires_client_cert());
4104    }
4105
4106    #[cfg(feature = "rustls")]
4107    #[tokio::test]
4108    async fn test_client_cert_verifier_optional_with_pem() {
4109        let _ = rustls::crypto::ring::default_provider().install_default();
4110        let ca = rcgen::generate_simple_self_signed(vec!["ANS Test CA".to_string()]).unwrap();
4111        let ca_pem = ca.cert.pem();
4112
4113        let dns = Arc::new(MockDnsResolver::new());
4114        let tlog = Arc::new(MockTransparencyLogClient::new());
4115
4116        let verifier = AnsVerifier::builder()
4117            .dns_resolver(dns as Arc<dyn DnsResolver>)
4118            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4119            .private_ca_pem(ca_pem.as_bytes().to_vec())
4120            .build()
4121            .await
4122            .unwrap();
4123
4124        let cv = verifier.client_cert_verifier_optional().unwrap();
4125        assert!(!cv.requires_client_cert());
4126    }
4127
4128    #[cfg(feature = "rustls")]
4129    #[tokio::test]
4130    async fn test_server_cert_verifier() {
4131        let _ = rustls::crypto::ring::default_provider().install_default();
4132        let dns = Arc::new(MockDnsResolver::new());
4133        let tlog = Arc::new(MockTransparencyLogClient::new());
4134
4135        let verifier = AnsVerifier::builder()
4136            .dns_resolver(dns as Arc<dyn DnsResolver>)
4137            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4138            .build()
4139            .await
4140            .unwrap();
4141
4142        let fp = CertFingerprint::parse(
4143            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
4144        )
4145        .unwrap();
4146        let sv = verifier.server_cert_verifier(&fp).unwrap();
4147        assert_eq!(sv.expected_fingerprint(), &fp);
4148    }
4149
4150    // =========================================================================
4151    // 7d: Builder configuration methods
4152    // =========================================================================
4153
4154    #[tokio::test]
4155    async fn test_builder_with_caching() {
4156        let dns = Arc::new(MockDnsResolver::new());
4157        let tlog = Arc::new(MockTransparencyLogClient::new());
4158
4159        let verifier = AnsVerifier::builder()
4160            .dns_resolver(dns as Arc<dyn DnsResolver>)
4161            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4162            .with_caching()
4163            .build()
4164            .await
4165            .unwrap();
4166
4167        // Verify the verifier was built with caching
4168        assert!(format!("{verifier:?}").contains("has_cache"));
4169    }
4170
4171    #[tokio::test]
4172    async fn test_builder_with_cache_config() {
4173        let dns = Arc::new(MockDnsResolver::new());
4174        let tlog = Arc::new(MockTransparencyLogClient::new());
4175
4176        let verifier = AnsVerifier::builder()
4177            .dns_resolver(dns as Arc<dyn DnsResolver>)
4178            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4179            .with_cache_config(CacheConfig::default())
4180            .build()
4181            .await
4182            .unwrap();
4183
4184        assert!(format!("{verifier:?}").contains("AnsVerifier"));
4185    }
4186
4187    #[tokio::test]
4188    async fn test_builder_with_dane_if_present() {
4189        let dns = Arc::new(MockDnsResolver::new());
4190        let tlog = Arc::new(MockTransparencyLogClient::new());
4191
4192        let verifier = ServerVerifier::builder()
4193            .dns_resolver(dns as Arc<dyn DnsResolver>)
4194            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4195            .with_dane_if_present()
4196            .build()
4197            .await
4198            .unwrap();
4199
4200        assert_eq!(verifier.dane_policy, DanePolicy::ValidateIfPresent);
4201    }
4202
4203    #[tokio::test]
4204    async fn test_builder_require_dane() {
4205        let dns = Arc::new(MockDnsResolver::new());
4206        let tlog = Arc::new(MockTransparencyLogClient::new());
4207
4208        let verifier = ServerVerifier::builder()
4209            .dns_resolver(dns as Arc<dyn DnsResolver>)
4210            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4211            .require_dane()
4212            .build()
4213            .await
4214            .unwrap();
4215
4216        assert_eq!(verifier.dane_policy, DanePolicy::Required);
4217    }
4218
4219    #[tokio::test]
4220    async fn test_builder_dane_port() {
4221        let dns = Arc::new(MockDnsResolver::new());
4222        let tlog = Arc::new(MockTransparencyLogClient::new());
4223
4224        let verifier = ServerVerifier::builder()
4225            .dns_resolver(dns as Arc<dyn DnsResolver>)
4226            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4227            .dane_port(8443)
4228            .build()
4229            .await
4230            .unwrap();
4231
4232        assert_eq!(verifier.dane_port, 8443);
4233    }
4234
4235    #[tokio::test]
4236    async fn test_builder_trusted_ra_domains() {
4237        let dns = Arc::new(MockDnsResolver::new());
4238        let tlog = Arc::new(MockTransparencyLogClient::new());
4239
4240        let verifier = ServerVerifier::builder()
4241            .dns_resolver(dns as Arc<dyn DnsResolver>)
4242            .tlog_client(tlog as Arc<dyn TransparencyLogClient>)
4243            .trusted_ra_domains(["tlog.example.com"])
4244            .build()
4245            .await
4246            .unwrap();
4247
4248        assert!(verifier.trusted_ra_domains.is_some());
4249        assert!(
4250            verifier
4251                .trusted_ra_domains
4252                .unwrap()
4253                .contains("tlog.example.com")
4254        );
4255    }
4256
4257    // =========================================================================
4258    // 7e: DANE Required failure path
4259    // =========================================================================
4260
4261    #[tokio::test]
4262    async fn test_dane_required_no_tlsa_records() {
4263        let host = "test.example.com";
4264        let fingerprint = "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904";
4265
4266        let badge = create_test_badge(host, "v1.0.0", fingerprint, "SHA256:aaa");
4267        let badge_url = "https://tlog.example.com/v1/agents/test-id";
4268
4269        let dns_record = BadgeRecord {
4270            format_version: "ans-badge1".to_string(),
4271            version: Some(Version::new(1, 0, 0)),
4272            url: badge_url.to_string(),
4273        };
4274
4275        // No TLSA records configured — DANE Required should fail
4276        let dns_resolver = Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
4277        let tlog_client = Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
4278
4279        let verifier = ServerVerifier {
4280            dns_resolver,
4281            tlog_client,
4282            cache: None,
4283            failure_policy: FailurePolicy::FailClosed,
4284            dane_policy: DanePolicy::Required,
4285            dane_port: 443,
4286            trusted_ra_domains: None,
4287        };
4288
4289        let cert = create_test_cert_identity(host, fingerprint);
4290        let fqdn = Fqdn::new(host).unwrap();
4291
4292        let outcome = verifier.verify(&fqdn, &cert).await;
4293        assert!(
4294            matches!(outcome, VerificationOutcome::DaneError(_)),
4295            "Expected DaneError for required DANE with no TLSA records, got: {outcome:?}"
4296        );
4297    }
4298
4299    // =========================================================================
4300    // VerificationOutcome helpers
4301    // =========================================================================
4302
4303    #[test]
4304    fn test_outcome_badge_returns_none_for_errors() {
4305        let outcome = VerificationOutcome::DnsError(DnsError::Timeout {
4306            fqdn: "test.example.com".to_string(),
4307        });
4308        assert!(outcome.badge().is_none());
4309
4310        let outcome = VerificationOutcome::NotAnsAgent {
4311            fqdn: "test.example.com".to_string(),
4312        };
4313        assert!(outcome.badge().is_none());
4314    }
4315
4316    #[test]
4317    fn test_outcome_badge_returns_some_for_mismatches() {
4318        let badge = create_test_badge(
4319            "test.example.com",
4320            "v1.0.0",
4321            "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
4322            "SHA256:aaa",
4323        );
4324
4325        let outcome = VerificationOutcome::HostnameMismatch {
4326            expected: "test.example.com".to_string(),
4327            actual: "other.example.com".to_string(),
4328            badge,
4329        };
4330        assert!(outcome.badge().is_some());
4331    }
4332
4333    #[test]
4334    fn test_server_verifier_debug_format() {
4335        let dbg = format!("{:?}", ServerVerifierBuilder::default());
4336        assert!(dbg.contains("ServerVerifierBuilder"));
4337    }
4338
4339    // ── SCITT integration tests ─────────────────────────────────────────────
4340
4341    #[cfg(feature = "scitt")]
4342    mod scitt_integration {
4343        use super::*;
4344        use crate::scitt::{
4345            RefreshableKeyStore, ScittError, ScittHeaders, ScittKeyStore,
4346            compute_sig_structure_digest, verify_status_token,
4347        };
4348        use base64::prelude::{BASE64_STANDARD, Engine as _};
4349        use p256::ecdsa::{SigningKey, signature::hazmat::PrehashSigner as _};
4350        use p256::pkcs8::EncodePublicKey as _;
4351        use sha2::{Digest, Sha256};
4352
4353        // ── Test helpers ────────────────────────────────────────────────
4354
4355        fn make_key_and_store(seed: u8) -> (SigningKey, ScittKeyStore) {
4356            let signing_key = SigningKey::from_slice(&[seed; 32]).unwrap();
4357            let verifying_key = signing_key.verifying_key();
4358            let spki_doc = verifying_key.to_public_key_der().unwrap();
4359            let spki_der = spki_doc.as_bytes();
4360            let digest = Sha256::digest(spki_der);
4361            let kid: [u8; 4] = [digest[0], digest[1], digest[2], digest[3]];
4362            let key_hash_hex = hex::encode(kid);
4363            let spki_b64 = BASE64_STANDARD.encode(spki_der);
4364            let key_string = format!("tl.example.com+{key_hash_hex}+{spki_b64}");
4365            let store = ScittKeyStore::from_c2sp_keys(&[key_string]).unwrap();
4366            (signing_key, store)
4367        }
4368
4369        fn build_protected_bytes(signing_key: &SigningKey) -> Vec<u8> {
4370            let spki_doc = signing_key.verifying_key().to_public_key_der().unwrap();
4371            let spki_der = spki_doc.as_bytes();
4372            let digest = Sha256::digest(spki_der);
4373            let kid = vec![digest[0], digest[1], digest[2], digest[3]];
4374            let pairs = vec![
4375                (
4376                    ciborium::Value::Integer(1.into()),
4377                    ciborium::Value::Integer((-7_i64).into()),
4378                ),
4379                (
4380                    ciborium::Value::Integer(4.into()),
4381                    ciborium::Value::Bytes(kid),
4382                ),
4383            ];
4384            let map = ciborium::Value::Map(pairs);
4385            let mut buf = Vec::new();
4386            ciborium::ser::into_writer(&map, &mut buf).unwrap();
4387            buf
4388        }
4389
4390        fn build_cbor_payload(
4391            agent_id: &str,
4392            status: &str,
4393            iat: i64,
4394            exp: i64,
4395            ans_name: &str,
4396            identity_certs: &[(String, String)],
4397            server_certs: &[(String, String)],
4398        ) -> Vec<u8> {
4399            let mut pairs: Vec<(ciborium::Value, ciborium::Value)> = Vec::new();
4400            pairs.push((
4401                ciborium::Value::Integer(1.into()),
4402                ciborium::Value::Text(agent_id.to_string()),
4403            ));
4404            pairs.push((
4405                ciborium::Value::Integer(2.into()),
4406                ciborium::Value::Text(status.to_string()),
4407            ));
4408            pairs.push((
4409                ciborium::Value::Integer(3.into()),
4410                ciborium::Value::Integer(iat.into()),
4411            ));
4412            pairs.push((
4413                ciborium::Value::Integer(4.into()),
4414                ciborium::Value::Integer(exp.into()),
4415            ));
4416            pairs.push((
4417                ciborium::Value::Integer(5.into()),
4418                ciborium::Value::Text(ans_name.to_string()),
4419            ));
4420            let id_certs: Vec<ciborium::Value> = identity_certs
4421                .iter()
4422                .map(|(fp, ct)| {
4423                    ciborium::Value::Map(vec![
4424                        (
4425                            ciborium::Value::Text("fingerprint".to_string()),
4426                            ciborium::Value::Text(fp.clone()),
4427                        ),
4428                        (
4429                            ciborium::Value::Text("cert_type".to_string()),
4430                            ciborium::Value::Text(ct.clone()),
4431                        ),
4432                    ])
4433                })
4434                .collect();
4435            pairs.push((
4436                ciborium::Value::Integer(6.into()),
4437                ciborium::Value::Array(id_certs),
4438            ));
4439            let srv_certs: Vec<ciborium::Value> = server_certs
4440                .iter()
4441                .map(|(fp, ct)| {
4442                    ciborium::Value::Map(vec![
4443                        (
4444                            ciborium::Value::Text("fingerprint".to_string()),
4445                            ciborium::Value::Text(fp.clone()),
4446                        ),
4447                        (
4448                            ciborium::Value::Text("cert_type".to_string()),
4449                            ciborium::Value::Text(ct.clone()),
4450                        ),
4451                    ])
4452                })
4453                .collect();
4454            pairs.push((
4455                ciborium::Value::Integer(7.into()),
4456                ciborium::Value::Array(srv_certs),
4457            ));
4458            pairs.push((
4459                ciborium::Value::Integer(8.into()),
4460                ciborium::Value::Map(vec![]),
4461            ));
4462            let map = ciborium::Value::Map(pairs);
4463            let mut buf = Vec::new();
4464            ciborium::ser::into_writer(&map, &mut buf).unwrap();
4465            buf
4466        }
4467
4468        fn make_token(signing_key: &SigningKey, payload: &[u8]) -> Vec<u8> {
4469            let protected_bytes = build_protected_bytes(signing_key);
4470            let digest = compute_sig_structure_digest(&protected_bytes, payload).unwrap();
4471            let (sig, _): (p256::ecdsa::Signature, _) = signing_key.sign_prehash(&digest).unwrap();
4472            let sig_bytes = sig.to_bytes().to_vec();
4473            let array = ciborium::Value::Array(vec![
4474                ciborium::Value::Bytes(protected_bytes),
4475                ciborium::Value::Map(vec![]),
4476                ciborium::Value::Bytes(payload.to_vec()),
4477                ciborium::Value::Bytes(sig_bytes),
4478            ]);
4479            let mut buf = Vec::new();
4480            ciborium::ser::into_writer(&array, &mut buf).unwrap();
4481            buf
4482        }
4483
4484        fn future_exp() -> i64 {
4485            4_102_444_800 // 2100-01-01 00:00:00 UTC
4486        }
4487
4488        fn past_exp() -> i64 {
4489            946_684_800 // 2000-01-01 00:00:00 UTC
4490        }
4491
4492        fn nil_uuid() -> String {
4493            "00000000-0000-0000-0000-000000000000".to_string()
4494        }
4495
4496        fn test_fp() -> String {
4497            format!("SHA256:{}", "00".repeat(32))
4498        }
4499
4500        fn test_fp2() -> String {
4501            format!("SHA256:{}", "11".repeat(32))
4502        }
4503
4504        fn make_verifier_with_scitt(
4505            host: &str,
4506            badge_fingerprint: &str,
4507            key_store: Arc<ScittKeyStore>,
4508            tier_policy: ScittTierPolicy,
4509        ) -> AnsVerifier {
4510            let identity_fp = format!("SHA256:{}", "22".repeat(32));
4511            let badge = create_test_badge(host, "v1.0.0", badge_fingerprint, &identity_fp);
4512            let badge_url = "https://tlog.example.com/v1/agents/test-id";
4513            let dns_record = BadgeRecord {
4514                format_version: "ans-badge1".to_string(),
4515                version: Some(Version::new(1, 0, 0)),
4516                url: badge_url.to_string(),
4517            };
4518            let dns_resolver =
4519                Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
4520            let tlog_client =
4521                Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
4522
4523            let server_verifier = ServerVerifier {
4524                dns_resolver: dns_resolver.clone(),
4525                tlog_client: tlog_client.clone(),
4526                cache: None,
4527                failure_policy: FailurePolicy::FailClosed,
4528                dane_policy: DanePolicy::Disabled,
4529                dane_port: 443,
4530                trusted_ra_domains: None,
4531            };
4532            let client_verifier = ClientVerifier {
4533                dns_resolver,
4534                tlog_client,
4535                cache: None,
4536                failure_policy: FailurePolicy::FailClosed,
4537                trusted_ra_domains: None,
4538            };
4539
4540            AnsVerifier {
4541                server_verifier,
4542                client_verifier,
4543                #[cfg(feature = "rustls")]
4544                private_ca_pem: None,
4545                scitt_config: Some(ScittConfig::new().with_tier_policy(tier_policy)),
4546                scitt_key_store: Some(Arc::new(RefreshableKeyStore::from_static(
4547                    (*key_store).clone(),
4548                ))),
4549                scitt_verification_cache: None,
4550            }
4551        }
4552
4553        fn make_valid_token(signing_key: &SigningKey, server_fp: &str) -> Vec<u8> {
4554            let payload = build_cbor_payload(
4555                &nil_uuid(),
4556                "ACTIVE",
4557                0,
4558                future_exp(),
4559                "ans://v1.0.0.agent.example.com",
4560                &[],
4561                &[(server_fp.to_string(), "X509-DV-SERVER".to_string())],
4562            );
4563            make_token(signing_key, &payload)
4564        }
4565
4566        fn make_valid_identity_token(signing_key: &SigningKey, identity_fp: &str) -> Vec<u8> {
4567            let payload = build_cbor_payload(
4568                &nil_uuid(),
4569                "ACTIVE",
4570                0,
4571                future_exp(),
4572                "ans://v1.0.0.agent.example.com",
4573                &[(identity_fp.to_string(), "X509-OV-CLIENT".to_string())],
4574                &[],
4575            );
4576            make_token(signing_key, &payload)
4577        }
4578
4579        // ── ScittConfig / ScittTierPolicy tests ─────────────────────────
4580
4581        #[test]
4582        fn scitt_config_default() {
4583            let config = ScittConfig::default();
4584            assert!(matches!(
4585                config.tier_policy,
4586                ScittTierPolicy::ScittWithBadgeFallback
4587            ));
4588            assert_eq!(config.clock_skew_tolerance, Duration::from_secs(60));
4589        }
4590
4591        #[test]
4592        fn scitt_config_builder_chain() {
4593            let config = ScittConfig::new()
4594                .with_tier_policy(ScittTierPolicy::RequireScitt)
4595                .with_clock_skew(Duration::from_secs(120));
4596            assert!(matches!(config.tier_policy, ScittTierPolicy::RequireScitt));
4597            assert_eq!(config.clock_skew_tolerance, Duration::from_secs(120));
4598        }
4599
4600        // ── VerificationOutcome SCITT variants ──────────────────────────
4601
4602        #[test]
4603        fn scitt_verified_is_success() {
4604            let (signing_key, store) = make_key_and_store(1);
4605            let token_bytes = make_valid_token(&signing_key, &test_fp());
4606            let verified =
4607                verify_status_token(&token_bytes, &store, Duration::from_secs(0)).unwrap();
4608
4609            let outcome = VerificationOutcome::ScittVerified {
4610                status_token: verified,
4611                tier: ans_types::VerificationTier::FullScitt,
4612                matched_fingerprint: CertFingerprint::parse(&test_fp()).unwrap(),
4613                badge: None,
4614            };
4615            assert!(outcome.is_success());
4616            assert!(!outcome.is_not_ans_agent());
4617        }
4618
4619        #[test]
4620        fn scitt_verified_badge_accessor_with_badge() {
4621            let (signing_key, store) = make_key_and_store(1);
4622            let token_bytes = make_valid_token(&signing_key, &test_fp());
4623            let verified =
4624                verify_status_token(&token_bytes, &store, Duration::from_secs(0)).unwrap();
4625
4626            let badge = create_test_badge("agent.example.com", "v1.0.0", &test_fp(), "SHA256:aaa");
4627            let outcome = VerificationOutcome::ScittVerified {
4628                status_token: verified,
4629                tier: ans_types::VerificationTier::FullScitt,
4630                matched_fingerprint: CertFingerprint::parse(&test_fp()).unwrap(),
4631                badge: Some(badge),
4632            };
4633            assert!(outcome.badge().is_some());
4634        }
4635
4636        #[test]
4637        fn scitt_verified_badge_accessor_without_badge() {
4638            let (signing_key, store) = make_key_and_store(1);
4639            let token_bytes = make_valid_token(&signing_key, &test_fp());
4640            let verified =
4641                verify_status_token(&token_bytes, &store, Duration::from_secs(0)).unwrap();
4642
4643            let outcome = VerificationOutcome::ScittVerified {
4644                status_token: verified,
4645                tier: ans_types::VerificationTier::StatusTokenVerified,
4646                matched_fingerprint: CertFingerprint::parse(&test_fp()).unwrap(),
4647                badge: None,
4648            };
4649            assert!(outcome.badge().is_none());
4650        }
4651
4652        #[test]
4653        fn scitt_error_is_not_success() {
4654            let outcome = VerificationOutcome::ScittError(ScittError::SignatureInvalid);
4655            assert!(!outcome.is_success());
4656        }
4657
4658        #[test]
4659        fn scitt_error_into_result() {
4660            let outcome = VerificationOutcome::ScittError(ScittError::SignatureInvalid);
4661            let result = outcome.into_result();
4662            assert!(result.is_err());
4663        }
4664
4665        #[test]
4666        fn scitt_verified_into_scitt_result_with_badge() {
4667            let (signing_key, store) = make_key_and_store(1);
4668            let token_bytes = make_valid_token(&signing_key, &test_fp());
4669            let verified =
4670                verify_status_token(&token_bytes, &store, Duration::from_secs(0)).unwrap();
4671            let badge = create_test_badge("agent.example.com", "v1.0.0", &test_fp(), "SHA256:aaa");
4672
4673            let outcome = VerificationOutcome::ScittVerified {
4674                status_token: verified,
4675                tier: ans_types::VerificationTier::FullScitt,
4676                matched_fingerprint: CertFingerprint::parse(&test_fp()).unwrap(),
4677                badge: Some(badge),
4678            };
4679            let result = outcome.into_scitt_result();
4680            assert!(result.is_ok());
4681            assert!(result.unwrap().is_some());
4682        }
4683
4684        #[test]
4685        fn scitt_verified_into_scitt_result_without_badge() {
4686            let (signing_key, store) = make_key_and_store(1);
4687            let token_bytes = make_valid_token(&signing_key, &test_fp());
4688            let verified =
4689                verify_status_token(&token_bytes, &store, Duration::from_secs(0)).unwrap();
4690
4691            let outcome = VerificationOutcome::ScittVerified {
4692                status_token: verified,
4693                tier: ans_types::VerificationTier::StatusTokenVerified,
4694                matched_fingerprint: CertFingerprint::parse(&test_fp()).unwrap(),
4695                badge: None,
4696            };
4697            // into_scitt_result is consistent with is_success(): both say "success"
4698            let result = outcome.into_scitt_result();
4699            assert!(result.is_ok());
4700            assert!(result.unwrap().is_none());
4701        }
4702
4703        #[test]
4704        fn scitt_error_into_scitt_result() {
4705            let outcome = VerificationOutcome::ScittError(ScittError::SignatureInvalid);
4706            let result = outcome.into_scitt_result();
4707            assert!(result.is_err());
4708        }
4709
4710        // ── verify_server_with_scitt ────────────────────────────────────
4711
4712        #[tokio::test]
4713        async fn scitt_server_verification_success_token_only() {
4714            let fp = test_fp();
4715            let (signing_key, store) = make_key_and_store(1);
4716            let store = Arc::new(store);
4717            let token_bytes = make_valid_token(&signing_key, &fp);
4718            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4719
4720            let verifier = make_verifier_with_scitt(
4721                "agent.example.com",
4722                &fp,
4723                store,
4724                ScittTierPolicy::ScittWithBadgeFallback,
4725            );
4726            let cert = create_test_cert_identity("agent.example.com", &fp);
4727            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4728
4729            let outcome = verifier
4730                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4731                .await;
4732            assert!(outcome.is_success());
4733            match outcome {
4734                VerificationOutcome::ScittVerified { tier, .. } => {
4735                    assert_eq!(tier, ans_types::VerificationTier::StatusTokenVerified);
4736                }
4737                other => panic!("Expected ScittVerified, got: {other:?}"),
4738            }
4739        }
4740
4741        #[tokio::test]
4742        async fn scitt_server_no_headers_fallback_to_badge() {
4743            let fp = test_fp();
4744            let (_, store) = make_key_and_store(1);
4745            let store = Arc::new(store);
4746
4747            let verifier = make_verifier_with_scitt(
4748                "agent.example.com",
4749                &fp,
4750                store,
4751                ScittTierPolicy::ScittWithBadgeFallback,
4752            );
4753            let cert = create_test_cert_identity("agent.example.com", &fp);
4754            let headers = ScittHeaders::new(None, None);
4755
4756            let outcome = verifier
4757                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4758                .await;
4759            // Should fall back to badge and succeed
4760            assert!(outcome.is_success());
4761            assert!(matches!(outcome, VerificationOutcome::Verified { .. }));
4762        }
4763
4764        #[tokio::test]
4765        async fn scitt_server_no_headers_require_scitt_fails() {
4766            let fp = test_fp();
4767            let (_, store) = make_key_and_store(1);
4768            let store = Arc::new(store);
4769
4770            let verifier = make_verifier_with_scitt(
4771                "agent.example.com",
4772                &fp,
4773                store,
4774                ScittTierPolicy::RequireScitt,
4775            );
4776            let cert = create_test_cert_identity("agent.example.com", &fp);
4777            let headers = ScittHeaders::new(None, None);
4778
4779            let outcome = verifier
4780                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4781                .await;
4782            assert!(!outcome.is_success());
4783            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4784        }
4785
4786        #[tokio::test]
4787        async fn scitt_server_corrupt_token_rejects() {
4788            let fp = test_fp();
4789            let (_, store) = make_key_and_store(1);
4790            let store = Arc::new(store);
4791            // Valid base64 but garbage COSE
4792            let bad_token_b64 = BASE64_STANDARD.encode(b"not-a-cose-structure");
4793
4794            let verifier = make_verifier_with_scitt(
4795                "agent.example.com",
4796                &fp,
4797                store,
4798                ScittTierPolicy::ScittWithBadgeFallback,
4799            );
4800            let cert = create_test_cert_identity("agent.example.com", &fp);
4801            let headers = ScittHeaders::from_base64(None, Some(&bad_token_b64)).unwrap();
4802
4803            let outcome = verifier
4804                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4805                .await;
4806            // Present but corrupt → hard reject, NOT badge fallback
4807            assert!(!outcome.is_success());
4808            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4809        }
4810
4811        #[tokio::test]
4812        async fn scitt_server_fingerprint_mismatch() {
4813            let fp = test_fp();
4814            let different_fp = test_fp2();
4815            let (signing_key, store) = make_key_and_store(1);
4816            let store = Arc::new(store);
4817            // Token lists fp, but cert has different_fp
4818            let token_bytes = make_valid_token(&signing_key, &fp);
4819            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4820
4821            let verifier = make_verifier_with_scitt(
4822                "agent.example.com",
4823                &fp,
4824                store,
4825                ScittTierPolicy::ScittWithBadgeFallback,
4826            );
4827            let cert = create_test_cert_identity("agent.example.com", &different_fp);
4828            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4829
4830            let outcome = verifier
4831                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4832                .await;
4833            assert!(!outcome.is_success());
4834            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4835        }
4836
4837        #[tokio::test]
4838        async fn scitt_server_expired_token_with_headers_rejects() {
4839            let fp = test_fp();
4840            let (signing_key, store) = make_key_and_store(1);
4841            let store = Arc::new(store);
4842            // Build an expired token
4843            let payload = build_cbor_payload(
4844                &nil_uuid(),
4845                "ACTIVE",
4846                0,
4847                past_exp(),
4848                "ans://v1.0.0.agent.example.com",
4849                &[],
4850                &[(fp.clone(), "X509-DV-SERVER".to_string())],
4851            );
4852            let token_bytes = make_token(&signing_key, &payload);
4853            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4854
4855            let verifier = make_verifier_with_scitt(
4856                "agent.example.com",
4857                &fp,
4858                store,
4859                ScittTierPolicy::ScittWithBadgeFallback,
4860            );
4861            let cert = create_test_cert_identity("agent.example.com", &fp);
4862            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4863
4864            let outcome = verifier
4865                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4866                .await;
4867            // Headers present + expired token = hard reject (no badge fallback)
4868            assert!(!outcome.is_success());
4869            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4870        }
4871
4872        #[tokio::test]
4873        async fn scitt_server_expired_token_require_scitt_fails() {
4874            let fp = test_fp();
4875            let (signing_key, store) = make_key_and_store(1);
4876            let store = Arc::new(store);
4877            let payload = build_cbor_payload(
4878                &nil_uuid(),
4879                "ACTIVE",
4880                0,
4881                past_exp(),
4882                "ans://v1.0.0.agent.example.com",
4883                &[],
4884                &[(fp.clone(), "X509-DV-SERVER".to_string())],
4885            );
4886            let token_bytes = make_token(&signing_key, &payload);
4887            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4888
4889            let verifier = make_verifier_with_scitt(
4890                "agent.example.com",
4891                &fp,
4892                store,
4893                ScittTierPolicy::RequireScitt,
4894            );
4895            let cert = create_test_cert_identity("agent.example.com", &fp);
4896            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4897
4898            let outcome = verifier
4899                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4900                .await;
4901            // RequireScitt + expired = hard fail
4902            assert!(!outcome.is_success());
4903            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4904        }
4905
4906        #[tokio::test]
4907        async fn scitt_server_terminal_status_rejects() {
4908            let fp = test_fp();
4909            let (signing_key, store) = make_key_and_store(1);
4910            let store = Arc::new(store);
4911            // Token with REVOKED status
4912            let payload = build_cbor_payload(
4913                &nil_uuid(),
4914                "REVOKED",
4915                0,
4916                future_exp(),
4917                "ans://v1.0.0.agent.example.com",
4918                &[],
4919                &[(fp.clone(), "X509-DV-SERVER".to_string())],
4920            );
4921            let token_bytes = make_token(&signing_key, &payload);
4922            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4923
4924            let verifier = make_verifier_with_scitt(
4925                "agent.example.com",
4926                &fp,
4927                store,
4928                ScittTierPolicy::ScittWithBadgeFallback,
4929            );
4930            let cert = create_test_cert_identity("agent.example.com", &fp);
4931            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4932
4933            let outcome = verifier
4934                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4935                .await;
4936            // Terminal status = hard reject even with fallback policy
4937            assert!(!outcome.is_success());
4938            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
4939        }
4940
4941        #[tokio::test]
4942        async fn scitt_server_badge_enhancement_policy() {
4943            let fp = test_fp();
4944            let (signing_key, store) = make_key_and_store(1);
4945            let store = Arc::new(store);
4946            let token_bytes = make_valid_token(&signing_key, &fp);
4947            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
4948
4949            let verifier = make_verifier_with_scitt(
4950                "agent.example.com",
4951                &fp,
4952                store,
4953                ScittTierPolicy::BadgeWithScittEnhancement,
4954            );
4955            let cert = create_test_cert_identity("agent.example.com", &fp);
4956            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
4957
4958            let outcome = verifier
4959                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4960                .await;
4961            // Badge succeeds, SCITT enhances → ScittVerified
4962            assert!(outcome.is_success());
4963            assert!(matches!(outcome, VerificationOutcome::ScittVerified { .. }));
4964        }
4965
4966        #[tokio::test]
4967        async fn scitt_server_badge_enhancement_no_headers() {
4968            let fp = test_fp();
4969            let (_, store) = make_key_and_store(1);
4970            let store = Arc::new(store);
4971
4972            let verifier = make_verifier_with_scitt(
4973                "agent.example.com",
4974                &fp,
4975                store,
4976                ScittTierPolicy::BadgeWithScittEnhancement,
4977            );
4978            let cert = create_test_cert_identity("agent.example.com", &fp);
4979            let headers = ScittHeaders::new(None, None);
4980
4981            let outcome = verifier
4982                .verify_server_with_scitt("agent.example.com", &cert, &headers)
4983                .await;
4984            // No SCITT headers → badge result only
4985            assert!(outcome.is_success());
4986            assert!(matches!(outcome, VerificationOutcome::Verified { .. }));
4987        }
4988
4989        // ── verify_client_with_scitt ────────────────────────────────────
4990
4991        #[tokio::test]
4992        async fn scitt_client_no_headers_fallback_to_badge() {
4993            let identity_fp = test_fp2(); // identity cert fingerprint
4994            let (_, store) = make_key_and_store(1);
4995            let store = Arc::new(store);
4996
4997            let verifier = make_verifier_with_scitt(
4998                "agent.example.com",
4999                &test_fp(), // server cert fp in badge
5000                store,
5001                ScittTierPolicy::ScittWithBadgeFallback,
5002            );
5003            // Client cert with URI SAN for mTLS
5004            let cert = CertIdentity {
5005                common_name: Some("agent.example.com".to_string()),
5006                dns_sans: vec!["agent.example.com".to_string()],
5007                uri_sans: vec!["ans://v1.0.0.agent.example.com".to_string()],
5008                fingerprint: CertFingerprint::parse(&identity_fp).unwrap(),
5009            };
5010            let headers = ScittHeaders::new(None, None);
5011
5012            let outcome = verifier.verify_client_with_scitt(&cert, &headers).await;
5013            // No SCITT headers → falls back to badge client verification.
5014            // Client verification tries to match identity cert fingerprint against
5015            // the badge's identity_cert fingerprint.
5016            // Our test badge uses "SHA256:00...00" as identity_fp, so this will
5017            // either match or not depending on the badge setup.
5018            // The key thing is it falls back (not ScittError).
5019            assert!(!matches!(outcome, VerificationOutcome::ScittError(_)));
5020        }
5021
5022        #[tokio::test]
5023        async fn scitt_client_no_headers_require_scitt_fails() {
5024            let identity_fp = test_fp2();
5025            let (_, store) = make_key_and_store(1);
5026            let store = Arc::new(store);
5027
5028            let verifier = make_verifier_with_scitt(
5029                "agent.example.com",
5030                &test_fp(),
5031                store,
5032                ScittTierPolicy::RequireScitt,
5033            );
5034            let cert = CertIdentity {
5035                common_name: Some("agent.example.com".to_string()),
5036                dns_sans: vec![],
5037                uri_sans: vec!["ans://v1.0.0.agent.example.com".to_string()],
5038                fingerprint: CertFingerprint::parse(&identity_fp).unwrap(),
5039            };
5040            let headers = ScittHeaders::new(None, None);
5041
5042            let outcome = verifier.verify_client_with_scitt(&cert, &headers).await;
5043            assert!(!outcome.is_success());
5044            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
5045        }
5046
5047        #[tokio::test]
5048        async fn scitt_client_verification_success_with_token() {
5049            let identity_fp = test_fp2();
5050            let (signing_key, store) = make_key_and_store(1);
5051            let store = Arc::new(store);
5052            let token_bytes = make_valid_identity_token(&signing_key, &identity_fp);
5053            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
5054
5055            let verifier = make_verifier_with_scitt(
5056                "agent.example.com",
5057                &test_fp(),
5058                store,
5059                ScittTierPolicy::ScittWithBadgeFallback,
5060            );
5061            let cert = CertIdentity {
5062                common_name: Some("agent.example.com".to_string()),
5063                dns_sans: vec!["agent.example.com".to_string()],
5064                uri_sans: vec!["ans://v1.0.0.agent.example.com".to_string()],
5065                fingerprint: CertFingerprint::parse(&identity_fp).unwrap(),
5066            };
5067            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
5068
5069            let outcome = verifier.verify_client_with_scitt(&cert, &headers).await;
5070            assert!(outcome.is_success());
5071            assert!(matches!(outcome, VerificationOutcome::ScittVerified { .. }));
5072        }
5073
5074        // ── Builder extensions ──────────────────────────────────────────
5075
5076        #[test]
5077        fn builder_scitt_config_sets_field() {
5078            let builder = AnsVerifier::builder()
5079                .scitt_config(ScittConfig::new().with_tier_policy(ScittTierPolicy::RequireScitt));
5080            assert!(builder.scitt_config.is_some());
5081            assert!(matches!(
5082                builder.scitt_config.unwrap().tier_policy,
5083                ScittTierPolicy::RequireScitt
5084            ));
5085        }
5086
5087        #[test]
5088        fn builder_scitt_key_store_sets_field() {
5089            let (_, store) = make_key_and_store(1);
5090            let builder = AnsVerifier::builder().scitt_key_store(Arc::new(store));
5091            assert!(builder.scitt_key_store.is_some());
5092        }
5093
5094        #[test]
5095        fn builder_debug_includes_scitt() {
5096            let builder = AnsVerifier::builder().scitt_config(ScittConfig::default());
5097            let dbg = format!("{builder:?}");
5098            assert!(dbg.contains("has_scitt_config"));
5099            assert!(dbg.contains("true"));
5100        }
5101
5102        // ── AnsVerifier debug with SCITT ────────────────────────────────
5103
5104        #[test]
5105        fn verifier_debug_includes_scitt() {
5106            let fp = test_fp();
5107            let (_, store) = make_key_and_store(1);
5108            let verifier = make_verifier_with_scitt(
5109                "agent.example.com",
5110                &fp,
5111                Arc::new(store),
5112                ScittTierPolicy::ScittWithBadgeFallback,
5113            );
5114            let dbg = format!("{verifier:?}");
5115            assert!(dbg.contains("has_scitt_config"));
5116        }
5117
5118        // ── No key store graceful fallback ──────────────────────────────
5119
5120        #[tokio::test]
5121        async fn scitt_no_key_store_falls_back_to_badge() {
5122            let fp = test_fp();
5123            let host = "agent.example.com";
5124            let badge = create_test_badge(host, "v1.0.0", &fp, "SHA256:aaa");
5125            let badge_url = "https://tlog.example.com/v1/agents/test-id";
5126            let dns_record = BadgeRecord {
5127                format_version: "ans-badge1".to_string(),
5128                version: Some(Version::new(1, 0, 0)),
5129                url: badge_url.to_string(),
5130            };
5131            let dns_resolver =
5132                Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
5133            let tlog_client =
5134                Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
5135
5136            let server_verifier = ServerVerifier {
5137                dns_resolver: dns_resolver.clone(),
5138                tlog_client: tlog_client.clone(),
5139                cache: None,
5140                failure_policy: FailurePolicy::FailClosed,
5141                dane_policy: DanePolicy::Disabled,
5142                dane_port: 443,
5143                trusted_ra_domains: None,
5144            };
5145            let client_verifier = ClientVerifier {
5146                dns_resolver,
5147                tlog_client,
5148                cache: None,
5149                failure_policy: FailurePolicy::FailClosed,
5150                trusted_ra_domains: None,
5151            };
5152
5153            // Config present, but NO key store
5154            let verifier = AnsVerifier {
5155                server_verifier,
5156                client_verifier,
5157                #[cfg(feature = "rustls")]
5158                private_ca_pem: None,
5159                scitt_config: Some(ScittConfig::default()),
5160                scitt_key_store: None,
5161                scitt_verification_cache: None,
5162            };
5163
5164            let cert = create_test_cert_identity(host, &fp);
5165            let headers = ScittHeaders::from_base64(None, Some("aGVsbG8=")).unwrap();
5166
5167            let outcome = verifier
5168                .verify_server_with_scitt(host, &cert, &headers)
5169                .await;
5170            // No key store → graceful badge fallback
5171            assert!(outcome.is_success());
5172            assert!(matches!(outcome, VerificationOutcome::Verified { .. }));
5173        }
5174
5175        // ── No SCITT config passes through to badge ─────────────────────
5176
5177        #[tokio::test]
5178        async fn scitt_no_config_passes_through() {
5179            let fp = test_fp();
5180            let host = "agent.example.com";
5181            let badge = create_test_badge(host, "v1.0.0", &fp, "SHA256:aaa");
5182            let badge_url = "https://tlog.example.com/v1/agents/test-id";
5183            let dns_record = BadgeRecord {
5184                format_version: "ans-badge1".to_string(),
5185                version: Some(Version::new(1, 0, 0)),
5186                url: badge_url.to_string(),
5187            };
5188            let dns_resolver =
5189                Arc::new(MockDnsResolver::new().with_records(host, vec![dns_record]));
5190            let tlog_client =
5191                Arc::new(MockTransparencyLogClient::new().with_badge(badge_url, badge));
5192
5193            let server_verifier = ServerVerifier {
5194                dns_resolver: dns_resolver.clone(),
5195                tlog_client: tlog_client.clone(),
5196                cache: None,
5197                failure_policy: FailurePolicy::FailClosed,
5198                dane_policy: DanePolicy::Disabled,
5199                dane_port: 443,
5200                trusted_ra_domains: None,
5201            };
5202            let client_verifier = ClientVerifier {
5203                dns_resolver,
5204                tlog_client,
5205                cache: None,
5206                failure_policy: FailurePolicy::FailClosed,
5207                trusted_ra_domains: None,
5208            };
5209
5210            let verifier = AnsVerifier {
5211                server_verifier,
5212                client_verifier,
5213                #[cfg(feature = "rustls")]
5214                private_ca_pem: None,
5215                scitt_config: None,
5216                scitt_key_store: None,
5217                scitt_verification_cache: None,
5218            };
5219
5220            let cert = create_test_cert_identity(host, &fp);
5221            let headers = ScittHeaders::from_base64(None, Some("aGVsbG8=")).unwrap();
5222
5223            let outcome = verifier
5224                .verify_server_with_scitt(host, &cert, &headers)
5225                .await;
5226            // No SCITT config → pass-through to badge
5227            assert!(outcome.is_success());
5228            assert!(matches!(outcome, VerificationOutcome::Verified { .. }));
5229        }
5230
5231        // ── Invalid FQDN ────────────────────────────────────────────────
5232
5233        #[tokio::test]
5234        async fn scitt_server_invalid_fqdn() {
5235            let (_, store) = make_key_and_store(1);
5236            let store = Arc::new(store);
5237
5238            let verifier = make_verifier_with_scitt(
5239                "agent.example.com",
5240                &test_fp(),
5241                store,
5242                ScittTierPolicy::ScittWithBadgeFallback,
5243            );
5244            let cert = create_test_cert_identity("agent.example.com", &test_fp());
5245            let headers = ScittHeaders::new(None, None);
5246
5247            let outcome = verifier.verify_server_with_scitt("", &cert, &headers).await;
5248            assert!(matches!(outcome, VerificationOutcome::ParseError(_)));
5249        }
5250
5251        // ── Wrong key rejects ───────────────────────────────────────────
5252
5253        #[tokio::test]
5254        async fn scitt_server_wrong_key_rejects() {
5255            let fp = test_fp();
5256            let (signing_key, _store) = make_key_and_store(1);
5257            let (_, wrong_store) = make_key_and_store(2); // Different key
5258            let wrong_store = Arc::new(wrong_store);
5259
5260            let token_bytes = make_valid_token(&signing_key, &fp);
5261            let token_b64 = BASE64_STANDARD.encode(&token_bytes);
5262
5263            let verifier = make_verifier_with_scitt(
5264                "agent.example.com",
5265                &fp,
5266                wrong_store,
5267                ScittTierPolicy::ScittWithBadgeFallback,
5268            );
5269            let cert = create_test_cert_identity("agent.example.com", &fp);
5270            let headers = ScittHeaders::from_base64(None, Some(&token_b64)).unwrap();
5271
5272            let outcome = verifier
5273                .verify_server_with_scitt("agent.example.com", &cert, &headers)
5274                .await;
5275            // Wrong key → crypto failure → hard reject (not fallback)
5276            assert!(!outcome.is_success());
5277            assert!(matches!(outcome, VerificationOutcome::ScittError(_)));
5278        }
5279    }
5280}