Skip to main content

ans_verify/
error.rs

1//! Error types for ANS verification.
2
3use ans_types::{BadgeStatus, CryptoError, ParseError};
4use thiserror::Error;
5
6/// Top-level error type for ANS operations.
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum AnsError {
10    /// DNS lookup or parsing error
11    #[error("DNS error: {0}")]
12    Dns(#[from] DnsError),
13
14    /// Transparency log API error
15    #[error("Transparency log error: {0}")]
16    TransparencyLog(#[from] TlogError),
17
18    /// Certificate parsing or cryptographic error
19    #[error("Certificate error: {0}")]
20    Certificate(#[from] CryptoError),
21
22    /// Verification logic error
23    #[error("Verification error: {0}")]
24    Verification(#[from] VerificationError),
25
26    /// Parse error for types
27    #[error("Parse error: {0}")]
28    Parse(#[from] ParseError),
29
30    /// SCITT verification error
31    #[cfg(feature = "scitt")]
32    #[error("SCITT error: {0}")]
33    Scitt(#[from] crate::scitt::ScittError),
34}
35
36/// Result type alias using `AnsError`.
37pub type AnsResult<T> = Result<T, AnsError>;
38
39/// DNS-specific errors.
40#[derive(Debug, Error, Clone)]
41#[non_exhaustive]
42pub enum DnsError {
43    /// Record does not exist (NXDOMAIN)
44    #[error("DNS record not found (NXDOMAIN) for {fqdn}")]
45    NotFound {
46        /// The FQDN that was queried.
47        fqdn: String,
48    },
49
50    /// DNS lookup failed (SERVFAIL, timeout, etc.)
51    #[error("DNS lookup failed for {fqdn}: {reason}")]
52    LookupFailed {
53        /// The FQDN that was queried.
54        fqdn: String,
55        /// The reason the lookup failed.
56        reason: String,
57    },
58
59    /// DNS query timed out
60    #[error("DNS query timed out for {fqdn}")]
61    Timeout {
62        /// The FQDN that timed out.
63        fqdn: String,
64    },
65
66    /// DNSSEC validation failed
67    #[error("DNSSEC validation failed for {fqdn}")]
68    DnssecFailed {
69        /// The FQDN that failed DNSSEC validation.
70        fqdn: String,
71    },
72
73    /// Invalid TXT record format
74    #[error("Invalid badge TXT record format: {record}")]
75    InvalidFormat {
76        /// The malformed DNS record content.
77        record: String,
78    },
79
80    /// Resolver configuration error
81    #[error("DNS resolver error: {0}")]
82    ResolverError(String),
83}
84
85/// HTTP transport error wrapper.
86///
87/// Wraps the underlying HTTP client error to avoid exposing third-party
88/// types in the public API.
89#[derive(Debug)]
90pub struct HttpError {
91    inner: reqwest::Error,
92}
93
94impl std::fmt::Display for HttpError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        self.inner.fmt(f)
97    }
98}
99
100impl std::error::Error for HttpError {
101    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102        self.inner.source()
103    }
104}
105
106impl From<reqwest::Error> for HttpError {
107    fn from(err: reqwest::Error) -> Self {
108        Self { inner: err }
109    }
110}
111
112/// Transparency log API errors.
113///
114/// These errors map to HTTP responses from the TL API:
115/// - 404 → `NotFound`
116/// - 5xx → `ServiceUnavailable`
117/// - Parse failures → `InvalidResponse`
118/// - Network/HTTP errors → `HttpError`
119#[derive(Debug, Error)]
120#[non_exhaustive]
121pub enum TlogError {
122    /// HTTP request failed (network error, timeout, etc.)
123    #[error("HTTP request failed: {0}")]
124    HttpError(#[from] HttpError),
125
126    /// Badge not found (HTTP 404)
127    #[error("Badge not found at {url}")]
128    NotFound {
129        /// The URL that returned 404.
130        url: String,
131    },
132
133    /// Invalid or unparseable badge response
134    #[error("Invalid badge response: {0}")]
135    InvalidResponse(String),
136
137    /// Service unavailable (HTTP 5xx)
138    #[error("Transparency log service unavailable")]
139    ServiceUnavailable,
140
141    /// Invalid URL construction
142    #[error("Invalid URL: {0}")]
143    InvalidUrl(String),
144
145    /// Invalid HTTP header name or value
146    #[error("Invalid header: {0}")]
147    InvalidHeader(String),
148
149    /// Badge URL domain is not in the trusted RA domains list
150    #[error("Untrusted badge domain: {domain}")]
151    UntrustedDomain {
152        /// The domain that was not trusted.
153        domain: String,
154        /// The list of trusted domains.
155        trusted: Vec<String>,
156    },
157}
158
159/// Verification logic errors.
160#[derive(Debug, Error, Clone)]
161#[non_exhaustive]
162pub enum VerificationError {
163    /// Badge status is not valid for connections
164    #[error("Badge status {status:?} is not valid for connections")]
165    InvalidStatus {
166        /// The badge status that was rejected.
167        status: BadgeStatus,
168    },
169
170    /// Certificate fingerprint does not match badge
171    #[error("Fingerprint mismatch: expected {expected}, got {actual}")]
172    FingerprintMismatch {
173        /// The expected fingerprint from the badge.
174        expected: String,
175        /// The actual fingerprint from the certificate.
176        actual: String,
177    },
178
179    /// Hostname does not match badge
180    #[error("Hostname mismatch: expected {expected}, got {actual}")]
181    HostnameMismatch {
182        /// The expected hostname from the badge.
183        expected: String,
184        /// The actual hostname from the certificate.
185        actual: String,
186    },
187
188    /// ANS name does not match badge
189    #[error("ANS name mismatch: expected {expected}, got {actual}")]
190    AnsNameMismatch {
191        /// The expected ANS name from the badge.
192        expected: String,
193        /// The actual ANS name from the certificate.
194        actual: String,
195    },
196
197    /// No matching badge found for the presented certificate version
198    #[error("No matching badge found for version {version}")]
199    NoMatchingBadge {
200        /// The version that had no matching badge.
201        version: String,
202    },
203
204    /// Certificate does not chain to trusted CA
205    #[error("Certificate does not chain to trusted CA")]
206    UntrustedCertificate,
207
208    /// DANE/TLSA verification failed
209    #[error("DANE verification failed: {0}")]
210    DaneVerificationFailed(DaneError),
211
212    /// Multiple errors occurred
213    #[error("Multiple verification errors: {errors:?}")]
214    Multiple {
215        /// The collected verification errors.
216        errors: Vec<Self>,
217    },
218
219    /// Builder or verifier configuration error
220    #[error("Configuration error: {0}")]
221    Configuration(String),
222}
223
224/// DANE/TLSA verification errors.
225#[derive(Debug, Error, Clone)]
226#[non_exhaustive]
227pub enum DaneError {
228    /// No TLSA records found when required
229    #[error("No TLSA records found for {fqdn}:{port}")]
230    NoTlsaRecords {
231        /// The FQDN that was queried.
232        fqdn: String,
233        /// The port that was queried.
234        port: u16,
235    },
236
237    /// TLSA record fingerprint does not match certificate
238    #[error("TLSA fingerprint mismatch: certificate not bound to DNS")]
239    FingerprintMismatch,
240
241    /// DNSSEC validation failed
242    #[error("DNSSEC validation failed for {fqdn}")]
243    DnssecValidationFailed {
244        /// The FQDN that failed DNSSEC validation.
245        fqdn: String,
246    },
247
248    /// DNSSEC required but not present
249    #[error("DNSSEC required but not present for {fqdn}")]
250    DnssecNotPresent {
251        /// The FQDN missing DNSSEC.
252        fqdn: String,
253    },
254
255    /// Invalid TLSA record format
256    #[error("Invalid TLSA record: {reason}")]
257    InvalidRecord {
258        /// The reason the record is invalid.
259        reason: String,
260    },
261
262    /// DNS lookup error during TLSA query
263    #[error("DNS error during TLSA lookup: {0}")]
264    DnsError(#[from] DnsError),
265}
266
267#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_dns_error_display() {
274        let error = DnsError::NotFound {
275            fqdn: "example.com".to_string(),
276        };
277        assert_eq!(
278            error.to_string(),
279            "DNS record not found (NXDOMAIN) for example.com"
280        );
281    }
282
283    #[test]
284    fn test_tlog_error_display() {
285        let error = TlogError::NotFound {
286            url: "https://example.com/badge".to_string(),
287        };
288        assert_eq!(
289            error.to_string(),
290            "Badge not found at https://example.com/badge"
291        );
292    }
293
294    #[test]
295    fn test_verification_error_display() {
296        let error = VerificationError::InvalidStatus {
297            status: BadgeStatus::Revoked.clone(),
298        };
299        assert_eq!(
300            error.to_string(),
301            "Badge status Revoked is not valid for connections"
302        );
303    }
304
305    #[test]
306    fn test_dane_error_display() {
307        let error = DaneError::NoTlsaRecords {
308            fqdn: "example.com".to_string(),
309            port: 443,
310        };
311        assert_eq!(
312            error.to_string(),
313            "No TLSA records found for example.com:443"
314        );
315    }
316
317    #[test]
318    fn test_multiple_verification_error_display() {
319        let error = VerificationError::Multiple {
320            errors: vec![
321                VerificationError::FingerprintMismatch {
322                    expected: "expected".to_string(),
323                    actual: "actual".to_string(),
324                },
325                VerificationError::InvalidStatus {
326                    status: BadgeStatus::Revoked.clone(),
327                },
328            ],
329        };
330        assert_eq!(
331            error.to_string(),
332            "Multiple verification errors: [FingerprintMismatch { expected: \"expected\", actual: \"actual\" }, InvalidStatus { status: Revoked }]"
333        );
334    }
335
336    #[test]
337    fn test_configuration_error_display() {
338        let error = VerificationError::Configuration("missing key".to_string());
339        assert_eq!(error.to_string(), "Configuration error: missing key");
340    }
341}