Commit
Author: Mauro D [mauro@stalw.art]
Hash: 23dbab9859a2590c0cd9318bd6d23a7dd03df584
Timestamp: Mon, 09 Jan 2023 08:49:37 +0000 (1 year ago)

+195 -56 +/-11 browse
IpRev implementation.
1diff --git a/src/arc/parse.rs b/src/arc/parse.rs
2index 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
52index 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
65index 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
97index 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
234index 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
261index 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
281index 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
356index 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
369index 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
410index 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
443index 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 }