Commit
+195 -56 +/-11 browse
1 | diff --git a/src/arc/parse.rs b/src/arc/parse.rs |
2 | index c8db680..4e1e526 100644 |
3 | --- a/src/arc/parse.rs |
4 | +++ b/src/arc/parse.rs |
5 | @@ -48,7 +48,7 @@ impl Signature { |
6 | I => { |
7 | signature.i = header.number().unwrap_or(0) as u32; |
8 | if !(1..=50).contains(&signature.i) { |
9 | - return Err(Error::ARCInvalidInstance(signature.i)); |
10 | + return Err(Error::ArcInvalidInstance(signature.i)); |
11 | } |
12 | } |
13 | A => { |
14 | @@ -133,22 +133,22 @@ impl Seal { |
15 | b'p' | b'P' if header.match_bytes(b"ass") => { |
16 | cv = ChainValidation::Pass.into(); |
17 | } |
18 | - _ => return Err(Error::ARCInvalidCV), |
19 | + _ => return Err(Error::ArcInvalidCV), |
20 | } |
21 | if !header.seek_tag_end() { |
22 | - return Err(Error::ARCInvalidCV); |
23 | + return Err(Error::ArcInvalidCV); |
24 | } |
25 | } |
26 | H => { |
27 | - return Err(Error::ARCHasHeaderTag); |
28 | + return Err(Error::ArcHasHeaderTag); |
29 | } |
30 | _ => header.ignore(), |
31 | } |
32 | } |
33 | - seal.cv = cv.ok_or(Error::ARCInvalidCV)?; |
34 | + seal.cv = cv.ok_or(Error::ArcInvalidCV)?; |
35 | |
36 | if !(1..=50).contains(&seal.i) { |
37 | - Err(Error::ARCInvalidInstance(seal.i)) |
38 | + Err(Error::ArcInvalidInstance(seal.i)) |
39 | } else if !seal.d.is_empty() && !seal.s.is_empty() && !seal.b.is_empty() { |
40 | Ok(seal) |
41 | } else { |
42 | @@ -176,7 +176,7 @@ impl Results { |
43 | if (1..=50).contains(&results.i) { |
44 | Ok(results) |
45 | } else { |
46 | - Err(Error::ARCInvalidInstance(results.i)) |
47 | + Err(Error::ArcInvalidInstance(results.i)) |
48 | } |
49 | } |
50 | } |
51 | diff --git a/src/arc/seal.rs b/src/arc/seal.rs |
52 | index 25eaf2f..88561f3 100644 |
53 | --- a/src/arc/seal.rs |
54 | +++ b/src/arc/seal.rs |
55 | @@ -29,7 +29,7 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> { |
56 | arc_output: &ArcOutput, |
57 | ) -> crate::Result<ArcSet<'x>> { |
58 | if !arc_output.can_be_sealed() { |
59 | - return Err(Error::ARCInvalidCV); |
60 | + return Err(Error::ArcInvalidCV); |
61 | } |
62 | |
63 | // Create set |
64 | diff --git a/src/arc/verify.rs b/src/arc/verify.rs |
65 | index db4f1ba..3322feb 100644 |
66 | --- a/src/arc/verify.rs |
67 | +++ b/src/arc/verify.rs |
68 | @@ -29,11 +29,11 @@ impl Resolver { |
69 | if arc_headers == 0 { |
70 | return ArcOutput::default(); |
71 | } else if arc_headers > 50 { |
72 | - return ArcOutput::default().with_result(DkimResult::Fail(Error::ARCChainTooLong)); |
73 | + return ArcOutput::default().with_result(DkimResult::Fail(Error::ArcChainTooLong)); |
74 | } else if (arc_headers != message.as_headers.len()) |
75 | || (arc_headers != message.aar_headers.len()) |
76 | { |
77 | - return ArcOutput::default().with_result(DkimResult::Fail(Error::ARCBrokenChain)); |
78 | + return ArcOutput::default().with_result(DkimResult::Fail(Error::ArcBrokenChain)); |
79 | } |
80 | |
81 | let now = SystemTime::now() |
82 | @@ -72,11 +72,11 @@ impl Resolver { |
83 | || (signature.i as usize != (pos + 1)) |
84 | || (results.i as usize != (pos + 1)) |
85 | { |
86 | - output.result = DkimResult::Fail(Error::ARCInvalidInstance((pos + 1) as u32)); |
87 | + output.result = DkimResult::Fail(Error::ArcInvalidInstance((pos + 1) as u32)); |
88 | } else if (pos == 0 && seal.cv != ChainValidation::None) |
89 | || (pos > 0 && seal.cv != ChainValidation::Pass) |
90 | { |
91 | - output.result = DkimResult::Fail(Error::ARCInvalidCV); |
92 | + output.result = DkimResult::Fail(Error::ArcInvalidCV); |
93 | } else if pos == arc_headers - 1 { |
94 | // Validate last signature in the chain |
95 | if signature.x == 0 || (signature.x > signature.t && signature.x > now) { |
96 | diff --git a/src/common/auth_results.rs b/src/common/auth_results.rs |
97 | index 865ecac..8764db3 100644 |
98 | --- a/src/common/auth_results.rs |
99 | +++ b/src/common/auth_results.rs |
100 | @@ -14,7 +14,7 @@ use mail_builder::encoders::base64::base64_encode; |
101 | |
102 | use crate::{ |
103 | ArcOutput, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput, DmarcResult, Error, |
104 | - ReceivedSpf, SpfOutput, SpfResult, |
105 | + IprevOutput, IprevResult, ReceivedSpf, SpfOutput, SpfResult, |
106 | }; |
107 | |
108 | use super::headers::{HeaderWriter, Writer}; |
109 | @@ -115,6 +115,13 @@ impl<'x> AuthenticationResults<'x> { |
110 | .ok(); |
111 | self |
112 | } |
113 | + |
114 | + pub fn with_iprev_result(mut self, iprev: &IprevOutput, remote_ip: IpAddr) -> Self { |
115 | + self.auth_results.push_str(";\r\n\tiprev="); |
116 | + iprev.result.as_auth_result(&mut self.auth_results); |
117 | + write!(self.auth_results, " policy.iprev={}", remote_ip).ok(); |
118 | + self |
119 | + } |
120 | } |
121 | |
122 | impl<'x> HeaderWriter for AuthenticationResults<'x> { |
123 | @@ -235,6 +242,27 @@ impl AsAuthResult for DmarcResult { |
124 | } |
125 | } |
126 | |
127 | + impl AsAuthResult for IprevResult { |
128 | + fn as_auth_result(&self, header: &mut String) { |
129 | + match &self { |
130 | + IprevResult::Pass => header.push_str("pass"), |
131 | + IprevResult::Fail(err) => { |
132 | + header.push_str("fail"); |
133 | + err.as_auth_result(header); |
134 | + } |
135 | + IprevResult::PermError(err) => { |
136 | + header.push_str("permerror"); |
137 | + err.as_auth_result(header); |
138 | + } |
139 | + IprevResult::TempError(err) => { |
140 | + header.push_str("temperror"); |
141 | + err.as_auth_result(header); |
142 | + } |
143 | + IprevResult::None => header.push_str("none"), |
144 | + } |
145 | + } |
146 | + } |
147 | + |
148 | impl AsAuthResult for DkimResult { |
149 | fn as_auth_result(&self, header: &mut String) { |
150 | match &self { |
151 | @@ -276,21 +304,21 @@ impl AsAuthResult for Error { |
152 | Error::UnsupportedKeyType => "unsupported key type", |
153 | Error::FailedBodyHashMatch => "body hash did not verify", |
154 | Error::FailedVerification => "verification failed", |
155 | - Error::FailedAUIDMatch => "auid does not match", |
156 | + Error::FailedAuidMatch => "auid does not match", |
157 | Error::RevokedPublicKey => "revoked public key", |
158 | Error::IncompatibleAlgorithms => "incompatible record/signature algorithms", |
159 | Error::SignatureExpired => "signature error", |
160 | Error::DnsError(_) => "dns error", |
161 | Error::DnsRecordNotFound(_) => "dns record not found", |
162 | - Error::ARCInvalidInstance(i) => { |
163 | + Error::ArcInvalidInstance(i) => { |
164 | write!(header, "invalid ARC instance {})", i).ok(); |
165 | return; |
166 | } |
167 | - Error::ARCInvalidCV => "invalid ARC cv", |
168 | - Error::ARCChainTooLong => "too many ARC headers", |
169 | - Error::ARCHasHeaderTag => "ARC has header tag", |
170 | - Error::ARCBrokenChain => "broken ARC chain", |
171 | - Error::DMARCNotAligned => "dmarc not aligned", |
172 | + Error::ArcInvalidCV => "invalid ARC cv", |
173 | + Error::ArcChainTooLong => "too many ARC headers", |
174 | + Error::ArcHasHeaderTag => "ARC has header tag", |
175 | + Error::ArcBrokenChain => "broken ARC chain", |
176 | + Error::NotAligned => "policy not aligned", |
177 | Error::InvalidRecordType => "invalid dns record type", |
178 | }); |
179 | header.push(')'); |
180 | @@ -301,7 +329,8 @@ impl AsAuthResult for Error { |
181 | mod test { |
182 | use crate::{ |
183 | dkim::Signature, dmarc::Policy, ArcOutput, AuthenticationResults, DkimOutput, DkimResult, |
184 | - DmarcOutput, DmarcResult, Error, ReceivedSpf, SpfOutput, SpfResult, |
185 | + DmarcOutput, DmarcResult, Error, IprevOutput, IprevResult, ReceivedSpf, SpfOutput, |
186 | + SpfResult, |
187 | }; |
188 | |
189 | #[test] |
190 | @@ -470,9 +499,9 @@ mod test { |
191 | }, |
192 | ), |
193 | ( |
194 | - "dmarc=fail (dmarc not aligned) header.from=example.com policy.dmarc=quarantine", |
195 | + "dmarc=fail (policy not aligned) header.from=example.com policy.dmarc=quarantine", |
196 | DmarcOutput { |
197 | - dkim_result: DmarcResult::Fail(Error::DMARCNotAligned), |
198 | + dkim_result: DmarcResult::Fail(Error::NotAligned), |
199 | spf_result: DmarcResult::None, |
200 | domain: "example.com".to_string(), |
201 | policy: Policy::Quarantine, |
202 | @@ -511,5 +540,30 @@ mod test { |
203 | expected_auth_results |
204 | ); |
205 | } |
206 | + |
207 | + for (expected_auth_results, iprev, remote_ip) in [ |
208 | + ( |
209 | + "iprev=pass policy.iprev=192.127.9.2", |
210 | + IprevOutput { |
211 | + result: IprevResult::Pass, |
212 | + ptr: None, |
213 | + }, |
214 | + "192.127.9.2".parse().unwrap(), |
215 | + ), |
216 | + ( |
217 | + "iprev=fail (policy not aligned) policy.iprev=1:2:3::a", |
218 | + IprevOutput { |
219 | + result: IprevResult::Fail(Error::NotAligned), |
220 | + ptr: None, |
221 | + }, |
222 | + "1:2:3::a".parse().unwrap(), |
223 | + ), |
224 | + ] { |
225 | + auth_results = auth_results.with_iprev_result(&iprev, remote_ip); |
226 | + assert_eq!( |
227 | + auth_results.auth_results.rsplit_once(';').unwrap().1.trim(), |
228 | + expected_auth_results |
229 | + ); |
230 | + } |
231 | } |
232 | } |
233 | diff --git a/src/common/mod.rs b/src/common/mod.rs |
234 | index e7a231c..8d55fbb 100644 |
235 | --- a/src/common/mod.rs |
236 | +++ b/src/common/mod.rs |
237 | @@ -8,6 +8,8 @@ |
238 | * except according to those terms. |
239 | */ |
240 | |
241 | + use crate::{Error, IprevResult}; |
242 | + |
243 | pub mod auth_results; |
244 | pub mod base32; |
245 | pub mod crypto; |
246 | @@ -17,3 +19,13 @@ pub mod message; |
247 | pub mod parse; |
248 | pub mod resolver; |
249 | pub mod verify; |
250 | + |
251 | + impl From<Error> for IprevResult { |
252 | + fn from(err: Error) -> Self { |
253 | + if matches!(&err, Error::DnsError(_)) { |
254 | + IprevResult::TempError(err) |
255 | + } else { |
256 | + IprevResult::PermError(err) |
257 | + } |
258 | + } |
259 | + } |
260 | diff --git a/src/common/resolver.rs b/src/common/resolver.rs |
261 | index 938abd5..28d355a 100644 |
262 | --- a/src/common/resolver.rs |
263 | +++ b/src/common/resolver.rs |
264 | @@ -255,7 +255,14 @@ impl Resolver { |
265 | let ptr = ptr_lookup |
266 | .as_lookup() |
267 | .record_iter() |
268 | - .filter_map(|r| r.data()?.as_ptr()?.to_lowercase().to_string().into()) |
269 | + .filter_map(|r| { |
270 | + let r = r.data()?.as_ptr()?; |
271 | + if !r.is_empty() { |
272 | + r.to_lowercase().to_string().into() |
273 | + } else { |
274 | + None |
275 | + } |
276 | + }) |
277 | .collect::<Vec<_>>(); |
278 | |
279 | Ok(self |
280 | diff --git a/src/common/verify.rs b/src/common/verify.rs |
281 | index 2f1bb97..1a3f1d6 100644 |
282 | --- a/src/common/verify.rs |
283 | +++ b/src/common/verify.rs |
284 | @@ -8,7 +8,9 @@ |
285 | * except according to those terms. |
286 | */ |
287 | |
288 | - use crate::dkim::Canonicalization; |
289 | + use std::net::IpAddr; |
290 | + |
291 | + use crate::{dkim::Canonicalization, Error, IprevOutput, IprevResult, Resolver}; |
292 | |
293 | use super::crypto::{Algorithm, VerifyingKey}; |
294 | |
295 | @@ -17,6 +19,59 @@ pub struct DomainKey { |
296 | pub(crate) f: u64, |
297 | } |
298 | |
299 | + impl Resolver { |
300 | + pub async fn verify_iprev(&self, addr: IpAddr) -> IprevOutput { |
301 | + match self.ptr_lookup(addr).await { |
302 | + Ok(ptr) => { |
303 | + let mut last_err = None; |
304 | + for host in ptr.iter().take(2) { |
305 | + match &addr { |
306 | + IpAddr::V4(ip) => match self.ipv4_lookup(host).await { |
307 | + Ok(ips) => { |
308 | + if ips.iter().any(|cip| cip == ip) { |
309 | + return IprevOutput { |
310 | + result: IprevResult::Pass, |
311 | + ptr: ptr.into(), |
312 | + }; |
313 | + } |
314 | + } |
315 | + Err(err) => { |
316 | + last_err = err.into(); |
317 | + } |
318 | + }, |
319 | + IpAddr::V6(ip) => match self.ipv6_lookup(host).await { |
320 | + Ok(ips) => { |
321 | + if ips.iter().any(|cip| cip == ip) { |
322 | + return IprevOutput { |
323 | + result: IprevResult::Pass, |
324 | + ptr: ptr.into(), |
325 | + }; |
326 | + } |
327 | + } |
328 | + Err(err) => { |
329 | + last_err = err.into(); |
330 | + } |
331 | + }, |
332 | + } |
333 | + } |
334 | + |
335 | + IprevOutput { |
336 | + result: if let Some(err) = last_err { |
337 | + err.into() |
338 | + } else { |
339 | + IprevResult::Fail(Error::NotAligned) |
340 | + }, |
341 | + ptr: ptr.into(), |
342 | + } |
343 | + } |
344 | + Err(err) => IprevOutput { |
345 | + result: err.into(), |
346 | + ptr: None, |
347 | + }, |
348 | + } |
349 | + } |
350 | + } |
351 | + |
352 | impl DomainKey { |
353 | pub(crate) fn verify<'a>( |
354 | &self, |
355 | diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs |
356 | index 05093a8..d264b1a 100644 |
357 | --- a/src/dkim/sign.rs |
358 | +++ b/src/dkim/sign.rs |
359 | @@ -275,7 +275,7 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc= |
360 | .sign(message.as_bytes()) |
361 | .unwrap(), |
362 | message, |
363 | - Err(super::Error::FailedAUIDMatch), |
364 | + Err(super::Error::FailedAuidMatch), |
365 | ) |
366 | .await; |
367 | |
368 | diff --git a/src/dkim/verify.rs b/src/dkim/verify.rs |
369 | index 402a659..626e9e1 100644 |
370 | --- a/src/dkim/verify.rs |
371 | +++ b/src/dkim/verify.rs |
372 | @@ -102,7 +102,7 @@ impl Resolver { |
373 | |
374 | // Enforce t=s flag |
375 | if !signature.validate_auid(&record) { |
376 | - output.push(DkimOutput::fail(Error::FailedAUIDMatch).with_signature(signature)); |
377 | + output.push(DkimOutput::fail(Error::FailedAuidMatch).with_signature(signature)); |
378 | continue; |
379 | } |
380 | |
381 | @@ -211,7 +211,7 @@ impl Resolver { |
382 | | Error::Io(_) |
383 | | Error::FailedVerification |
384 | | Error::FailedBodyHashMatch |
385 | - | Error::FailedAUIDMatch => (record.rr & RR_VERIFICATION) != 0, |
386 | + | Error::FailedAuidMatch => (record.rr & RR_VERIFICATION) != 0, |
387 | Error::Base64 |
388 | | Error::UnsupportedVersion |
389 | | Error::UnsupportedAlgorithm |
390 | @@ -226,12 +226,12 @@ impl Resolver { |
391 | | Error::RevokedPublicKey => (record.rr & RR_DNS) != 0, |
392 | Error::MissingParameters |
393 | | Error::NoHeadersFound |
394 | - | Error::ARCChainTooLong |
395 | - | Error::ARCInvalidInstance(_) |
396 | - | Error::ARCInvalidCV |
397 | - | Error::ARCHasHeaderTag |
398 | - | Error::ARCBrokenChain |
399 | - | Error::DMARCNotAligned => (record.rr & RR_OTHER) != 0, |
400 | + | Error::ArcChainTooLong |
401 | + | Error::ArcInvalidInstance(_) |
402 | + | Error::ArcInvalidCV |
403 | + | Error::ArcHasHeaderTag |
404 | + | Error::ArcBrokenChain |
405 | + | Error::NotAligned => (record.rr & RR_OTHER) != 0, |
406 | }; |
407 | |
408 | if send_report { |
409 | diff --git a/src/dmarc/verify.rs b/src/dmarc/verify.rs |
410 | index 9507ab7..2521bf5 100644 |
411 | --- a/src/dmarc/verify.rs |
412 | +++ b/src/dmarc/verify.rs |
413 | @@ -78,7 +78,7 @@ impl Resolver { |
414 | output.policy = dmarc.sp; |
415 | DmarcResult::Pass |
416 | } else { |
417 | - DmarcResult::Fail(Error::DMARCNotAligned) |
418 | + DmarcResult::Fail(Error::NotAligned) |
419 | }; |
420 | } |
421 | |
422 | @@ -107,7 +107,7 @@ impl Resolver { |
423 | }) { |
424 | output.policy = dmarc.sp; |
425 | } |
426 | - DmarcResult::Fail(Error::DMARCNotAligned) |
427 | + DmarcResult::Fail(Error::NotAligned) |
428 | }; |
429 | } |
430 | } |
431 | @@ -259,8 +259,8 @@ mod test { |
432 | "subdomain.example.org", |
433 | DkimResult::Pass, |
434 | SpfResult::Pass, |
435 | - DmarcResult::Fail(Error::DMARCNotAligned), |
436 | - DmarcResult::Fail(Error::DMARCNotAligned), |
437 | + DmarcResult::Fail(Error::NotAligned), |
438 | + DmarcResult::Fail(Error::NotAligned), |
439 | Policy::Quarantine, |
440 | ), |
441 | // Strict - Pass with tree walk |
442 | diff --git a/src/lib.rs b/src/lib.rs |
443 | index 10a08a4..9407d2c 100644 |
444 | --- a/src/lib.rs |
445 | +++ b/src/lib.rs |
446 | @@ -401,6 +401,21 @@ pub enum DmarcResult { |
447 | } |
448 | |
449 | #[derive(Debug, PartialEq, Eq, Clone)] |
450 | + pub struct IprevOutput { |
451 | + result: IprevResult, |
452 | + ptr: Option<Arc<Vec<String>>>, |
453 | + } |
454 | + |
455 | + #[derive(Debug, PartialEq, Eq, Clone)] |
456 | + pub enum IprevResult { |
457 | + Pass, |
458 | + Fail(crate::Error), |
459 | + TempError(crate::Error), |
460 | + PermError(crate::Error), |
461 | + None, |
462 | + } |
463 | + |
464 | + #[derive(Debug, PartialEq, Eq, Clone)] |
465 | pub(crate) enum Version { |
466 | V1, |
467 | } |
468 | @@ -419,22 +434,18 @@ pub enum Error { |
469 | UnsupportedKeyType, |
470 | FailedBodyHashMatch, |
471 | FailedVerification, |
472 | - FailedAUIDMatch, |
473 | + FailedAuidMatch, |
474 | RevokedPublicKey, |
475 | IncompatibleAlgorithms, |
476 | SignatureExpired, |
477 | - |
478 | DnsError(String), |
479 | DnsRecordNotFound(ResponseCode), |
480 | - |
481 | - ARCChainTooLong, |
482 | - ARCInvalidInstance(u32), |
483 | - ARCInvalidCV, |
484 | - ARCHasHeaderTag, |
485 | - ARCBrokenChain, |
486 | - |
487 | - DMARCNotAligned, |
488 | - |
489 | + ArcChainTooLong, |
490 | + ArcInvalidInstance(u32), |
491 | + ArcInvalidCV, |
492 | + ArcHasHeaderTag, |
493 | + ArcBrokenChain, |
494 | + NotAligned, |
495 | InvalidRecordType, |
496 | } |
497 | |
498 | @@ -467,18 +478,18 @@ impl Display for Error { |
499 | ), |
500 | Error::FailedVerification => write!(f, "Signature verification failed"), |
501 | Error::SignatureExpired => write!(f, "Signature expired"), |
502 | - Error::FailedAUIDMatch => write!(f, "AUID does not match domain name"), |
503 | - Error::ARCInvalidInstance(i) => { |
504 | + Error::FailedAuidMatch => write!(f, "AUID does not match domain name"), |
505 | + Error::ArcInvalidInstance(i) => { |
506 | write!(f, "Invalid 'i={}' value found in ARC header", i) |
507 | } |
508 | - Error::ARCInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header"), |
509 | - Error::ARCHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal"), |
510 | - Error::ARCBrokenChain => write!(f, "Broken or missing ARC chain"), |
511 | - Error::ARCChainTooLong => write!(f, "Too many ARC headers"), |
512 | + Error::ArcInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header"), |
513 | + Error::ArcHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal"), |
514 | + Error::ArcBrokenChain => write!(f, "Broken or missing ARC chain"), |
515 | + Error::ArcChainTooLong => write!(f, "Too many ARC headers"), |
516 | Error::InvalidRecordType => write!(f, "Invalid record"), |
517 | Error::DnsError(err) => write!(f, "DNS resolution error: {}", err), |
518 | Error::DnsRecordNotFound(code) => write!(f, "DNS record not found: {}", code), |
519 | - Error::DMARCNotAligned => write!(f, "DMARC policy not aligned"), |
520 | + Error::NotAligned => write!(f, "Policy not aligned"), |
521 | } |
522 | } |
523 | } |