1use ans_types::{BadgeStatus, CryptoError, ParseError};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum AnsError {
10 #[error("DNS error: {0}")]
12 Dns(#[from] DnsError),
13
14 #[error("Transparency log error: {0}")]
16 TransparencyLog(#[from] TlogError),
17
18 #[error("Certificate error: {0}")]
20 Certificate(#[from] CryptoError),
21
22 #[error("Verification error: {0}")]
24 Verification(#[from] VerificationError),
25
26 #[error("Parse error: {0}")]
28 Parse(#[from] ParseError),
29
30 #[cfg(feature = "scitt")]
32 #[error("SCITT error: {0}")]
33 Scitt(#[from] crate::scitt::ScittError),
34}
35
36pub type AnsResult<T> = Result<T, AnsError>;
38
39#[derive(Debug, Error, Clone)]
41#[non_exhaustive]
42pub enum DnsError {
43 #[error("DNS record not found (NXDOMAIN) for {fqdn}")]
45 NotFound {
46 fqdn: String,
48 },
49
50 #[error("DNS lookup failed for {fqdn}: {reason}")]
52 LookupFailed {
53 fqdn: String,
55 reason: String,
57 },
58
59 #[error("DNS query timed out for {fqdn}")]
61 Timeout {
62 fqdn: String,
64 },
65
66 #[error("DNSSEC validation failed for {fqdn}")]
68 DnssecFailed {
69 fqdn: String,
71 },
72
73 #[error("Invalid badge TXT record format: {record}")]
75 InvalidFormat {
76 record: String,
78 },
79
80 #[error("DNS resolver error: {0}")]
82 ResolverError(String),
83}
84
85#[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#[derive(Debug, Error)]
120#[non_exhaustive]
121pub enum TlogError {
122 #[error("HTTP request failed: {0}")]
124 HttpError(#[from] HttpError),
125
126 #[error("Badge not found at {url}")]
128 NotFound {
129 url: String,
131 },
132
133 #[error("Invalid badge response: {0}")]
135 InvalidResponse(String),
136
137 #[error("Transparency log service unavailable")]
139 ServiceUnavailable,
140
141 #[error("Invalid URL: {0}")]
143 InvalidUrl(String),
144
145 #[error("Invalid header: {0}")]
147 InvalidHeader(String),
148
149 #[error("Untrusted badge domain: {domain}")]
151 UntrustedDomain {
152 domain: String,
154 trusted: Vec<String>,
156 },
157}
158
159#[derive(Debug, Error, Clone)]
161#[non_exhaustive]
162pub enum VerificationError {
163 #[error("Badge status {status:?} is not valid for connections")]
165 InvalidStatus {
166 status: BadgeStatus,
168 },
169
170 #[error("Fingerprint mismatch: expected {expected}, got {actual}")]
172 FingerprintMismatch {
173 expected: String,
175 actual: String,
177 },
178
179 #[error("Hostname mismatch: expected {expected}, got {actual}")]
181 HostnameMismatch {
182 expected: String,
184 actual: String,
186 },
187
188 #[error("ANS name mismatch: expected {expected}, got {actual}")]
190 AnsNameMismatch {
191 expected: String,
193 actual: String,
195 },
196
197 #[error("No matching badge found for version {version}")]
199 NoMatchingBadge {
200 version: String,
202 },
203
204 #[error("Certificate does not chain to trusted CA")]
206 UntrustedCertificate,
207
208 #[error("DANE verification failed: {0}")]
210 DaneVerificationFailed(DaneError),
211
212 #[error("Multiple verification errors: {errors:?}")]
214 Multiple {
215 errors: Vec<Self>,
217 },
218
219 #[error("Configuration error: {0}")]
221 Configuration(String),
222}
223
224#[derive(Debug, Error, Clone)]
226#[non_exhaustive]
227pub enum DaneError {
228 #[error("No TLSA records found for {fqdn}:{port}")]
230 NoTlsaRecords {
231 fqdn: String,
233 port: u16,
235 },
236
237 #[error("TLSA fingerprint mismatch: certificate not bound to DNS")]
239 FingerprintMismatch,
240
241 #[error("DNSSEC validation failed for {fqdn}")]
243 DnssecValidationFailed {
244 fqdn: String,
246 },
247
248 #[error("DNSSEC required but not present for {fqdn}")]
250 DnssecNotPresent {
251 fqdn: String,
253 },
254
255 #[error("Invalid TLSA record: {reason}")]
257 InvalidRecord {
258 reason: String,
260 },
261
262 #[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}