Commit
Author: Mauro D [mauro@stalw.art]
Hash: 538325be9620d0372fb290f085eabc7be6fa7975
Timestamp: Thu, 10 Nov 2022 18:52:25 +0000 (2 years ago)

+919 -435 +/-18 browse
DNS resolver implementation.
1diff --git a/Cargo.toml b/Cargo.toml
2index 8f53ddc..ad8b2db 100644
3--- a/Cargo.toml
4+++ b/Cargo.toml
5 @@ -12,3 +12,10 @@ rsa = {version = "0.7.0"}
6 ed25519-dalek = "1.0.1"
7 sha1 = {version = "0.10", features = ["oid"]}
8 sha2 = {version = "0.10.6", features = ["oid"]}
9+ trust-dns-resolver = { version = "0.22.0", features = ["dns-over-rustls"] }
10+ lru-cache = "0.1.2"
11+ parking_lot = "0.12.0"
12+ ahash = "0.8.0"
13+
14+ [dev-dependencies]
15+ tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] }
16 diff --git a/src/arc/mod.rs b/src/arc/mod.rs
17index 4cdf6f9..18cd8d5 100644
18--- a/src/arc/mod.rs
19+++ b/src/arc/mod.rs
20 @@ -62,6 +62,14 @@ impl<'x> VerifySignature for Signature<'x> {
21 fn a(&self) -> Algorithm {
22 self.a
23 }
24+
25+ fn s(&self) -> &[u8] {
26+ &self.s
27+ }
28+
29+ fn d(&self) -> &[u8] {
30+ &self.d
31+ }
32 }
33
34 impl<'x> VerifySignature for Seal<'x> {
35 @@ -72,4 +80,12 @@ impl<'x> VerifySignature for Seal<'x> {
36 fn a(&self) -> Algorithm {
37 self.a
38 }
39+
40+ fn s(&self) -> &[u8] {
41+ &self.s
42+ }
43+
44+ fn d(&self) -> &[u8] {
45+ &self.d
46+ }
47 }
48 diff --git a/src/arc/parse.rs b/src/arc/parse.rs
49index 3bd46ee..611c483 100644
50--- a/src/arc/parse.rs
51+++ b/src/arc/parse.rs
52 @@ -10,7 +10,7 @@ use super::{ChainValidation, Results, Seal, Signature};
53
54 use crate::common::parse::*;
55
56- pub(crate) const CV: u16 = (b'c' as u16) | ((b'v' as u16) << 8);
57+ pub(crate) const CV: u64 = (b'c' as u64) | ((b'v' as u64) << 8);
58
59 impl<'x> Signature<'x> {
60 #[allow(clippy::while_let_on_iterator)]
61 diff --git a/src/common/lru.rs b/src/common/lru.rs
62new file mode 100644
63index 0000000..afb3155
64--- /dev/null
65+++ b/src/common/lru.rs
66 @@ -0,0 +1,55 @@
67+ use std::{borrow::Borrow, hash::Hash, time::Instant};
68+
69+ use parking_lot::Mutex;
70+
71+ pub(crate) type LruCache<K, V> = Mutex<lru_cache::LruCache<K, LruItem<V>, ahash::RandomState>>;
72+
73+ #[derive(Debug, Clone)]
74+ pub(crate) struct LruItem<V> {
75+ item: V,
76+ valid_until: Instant,
77+ }
78+
79+ pub(crate) trait DnsCache<K, V>: Sized {
80+ fn with_capacity(capacity: usize) -> Self;
81+ fn get<Q: ?Sized>(&self, name: &Q) -> Option<V>
82+ where
83+ K: Borrow<Q>,
84+ Q: Hash + Eq;
85+ fn insert(&self, name: K, value: V, valid_until: Instant) -> V;
86+ }
87+
88+ impl<K: Hash + Eq, V: Clone> DnsCache<K, V> for LruCache<K, V> {
89+ fn with_capacity(capacity: usize) -> Self {
90+ Mutex::new(lru_cache::LruCache::with_hasher(
91+ capacity,
92+ ahash::RandomState::new(),
93+ ))
94+ }
95+
96+ fn get<Q: ?Sized>(&self, name: &Q) -> Option<V>
97+ where
98+ K: Borrow<Q>,
99+ Q: Hash + Eq,
100+ {
101+ let mut cache = self.lock();
102+ let entry = cache.get_mut(name)?;
103+ if entry.valid_until >= Instant::now() {
104+ entry.item.clone().into()
105+ } else {
106+ cache.remove(name);
107+ None
108+ }
109+ }
110+
111+ fn insert(&self, name: K, item: V, valid_until: Instant) -> V {
112+ self.lock().insert(
113+ name,
114+ LruItem {
115+ item: item.clone(),
116+ valid_until,
117+ },
118+ );
119+ item
120+ }
121+ }
122 diff --git a/src/common/message.rs b/src/common/message.rs
123index 0764e75..57574e6 100644
124--- a/src/common/message.rs
125+++ b/src/common/message.rs
126 @@ -12,13 +12,13 @@ use crate::{
127
128 use super::{
129 headers::{AuthenticatedHeader, Header, HeaderParser},
130- AuthPhase, AuthResult, AuthenticatedMessage,
131+ AuthenticatedMessage,
132 };
133
134 impl<'x> AuthenticatedMessage<'x> {
135 #[inline(always)]
136- pub fn new(raw_message: &'x [u8]) -> Option<Self> {
137- Self::new_(
138+ pub fn parse(raw_message: &'x [u8]) -> Option<Self> {
139+ Self::parse_(
140 raw_message,
141 SystemTime::now()
142 .duration_since(SystemTime::UNIX_EPOCH)
143 @@ -27,15 +27,14 @@ impl<'x> AuthenticatedMessage<'x> {
144 )
145 }
146
147- pub(crate) fn new_(raw_message: &'x [u8], now: u64) -> Option<Self> {
148+ pub(crate) fn parse_(raw_message: &'x [u8], now: u64) -> Option<Self> {
149 let mut message = AuthenticatedMessage {
150 headers: Vec::new(),
151 from: Vec::new(),
152- dkim_headers: Vec::new(),
153- arc_sets: Vec::new(),
154- arc_result: AuthResult::None,
155- dkim_result: AuthResult::None,
156- phase: AuthPhase::Done,
157+ dkim_pass: Vec::new(),
158+ dkim_fail: Vec::new(),
159+ arc_pass: Vec::new(),
160+ arc_fail: Vec::new(),
161 };
162
163 let mut ams_headers = Vec::new();
164 @@ -54,7 +53,7 @@ impl<'x> AuthenticatedMessage<'x> {
165 {
166 dkim_headers.push(Header::new(name, value, signature));
167 } else {
168- message.dkim_result = AuthResult::PermFail(Header::new(
169+ message.dkim_fail.push(Header::new(
170 name,
171 value,
172 crate::Error::SignatureExpired,
173 @@ -62,8 +61,7 @@ impl<'x> AuthenticatedMessage<'x> {
174 }
175 }
176 Err(err) => {
177- message.dkim_result =
178- AuthResult::PermFail(Header::new(name, value, err));
179+ message.dkim_fail.push(Header::new(name, value, err));
180 }
181 }
182
183 @@ -75,8 +73,7 @@ impl<'x> AuthenticatedMessage<'x> {
184 aar_headers.push(Header::new(name, value, r));
185 }
186 Err(err) => {
187- message.arc_result =
188- AuthResult::PermFail(Header::new(name, value, err));
189+ message.arc_fail.push(Header::new(name, value, err));
190 }
191 }
192
193 @@ -88,8 +85,7 @@ impl<'x> AuthenticatedMessage<'x> {
194 ams_headers.push(Header::new(name, value, s));
195 }
196 Err(err) => {
197- message.arc_result =
198- AuthResult::PermFail(Header::new(name, value, err));
199+ message.arc_fail.push(Header::new(name, value, err));
200 }
201 }
202
203 @@ -101,8 +97,7 @@ impl<'x> AuthenticatedMessage<'x> {
204 as_headers.push(Header::new(name, value, s));
205 }
206 Err(err) => {
207- message.arc_result =
208- AuthResult::PermFail(Header::new(name, value, err));
209+ message.arc_fail.push(Header::new(name, value, err));
210 }
211 }
212 name
213 @@ -180,11 +175,12 @@ impl<'x> AuthenticatedMessage<'x> {
214 // Validate expiration
215 let signature_ = &signature.header;
216 if signature_.x > 0 && (signature_.x < signature_.t || signature_.x < now) {
217- message.arc_result = AuthResult::PermFail(Header::new(
218+ message.arc_fail.push(Header::new(
219 signature.name,
220 signature.value,
221 Error::SignatureExpired,
222 ));
223+ message.arc_pass.clear();
224 break;
225 }
226
227 @@ -208,52 +204,53 @@ impl<'x> AuthenticatedMessage<'x> {
228 ));
229
230 if !success {
231- message.arc_result = AuthResult::PermFail(Header::new(
232+ message.arc_fail.push(Header::new(
233 signature.name,
234 signature.value,
235 Error::FailedBodyHashMatch,
236 ));
237+ message.arc_pass.clear();
238 break;
239 }
240 }
241
242- message.arc_sets.push(Set {
243+ message.arc_pass.push(Set {
244 signature,
245 seal,
246 results,
247 });
248 } else {
249- message.arc_result = AuthResult::PermFail(Header::new(
250+ message.arc_fail.push(Header::new(
251 signature.name,
252 signature.value,
253 Error::ARCBrokenChain,
254 ));
255+ message.arc_pass.clear();
256 break;
257 }
258 }
259- } else if arc_headers > 0 && message.arc_result == AuthResult::None {
260+ } else if arc_headers > 0 {
261 // Missing ARC headers, fail all.
262- let header = ams_headers
263- .into_iter()
264- .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain))
265- .chain(
266- as_headers
267- .into_iter()
268- .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
269- )
270- .chain(
271- aar_headers
272- .into_iter()
273- .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
274- )
275- .next()
276- .unwrap();
277- message.arc_result = AuthResult::PermFail(header);
278+ message.arc_fail.extend(
279+ ams_headers
280+ .into_iter()
281+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain))
282+ .chain(
283+ as_headers
284+ .into_iter()
285+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
286+ )
287+ .chain(
288+ aar_headers
289+ .into_iter()
290+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
291+ ),
292+ );
293 }
294
295 // Validate body hash of DKIM signatures
296 if !dkim_headers.is_empty() {
297- message.dkim_headers = Vec::with_capacity(dkim_headers.len());
298+ message.dkim_pass = Vec::with_capacity(dkim_headers.len());
299 for header in dkim_headers {
300 let signature = &header.header;
301 let ha = HashAlgorithm::from(signature.a);
302 @@ -277,9 +274,9 @@ impl<'x> AuthenticatedMessage<'x> {
303 };
304
305 if bh == &signature.bh {
306- message.dkim_headers.push(header);
307+ message.dkim_pass.push(header);
308 } else {
309- message.dkim_result = AuthResult::PermFail(Header::new(
310+ message.dkim_fail.push(Header::new(
311 header.name,
312 header.value,
313 crate::Error::FailedBodyHashMatch,
314 @@ -288,13 +285,6 @@ impl<'x> AuthenticatedMessage<'x> {
315 }
316 }
317
318- if !message.dkim_headers.is_empty() {
319- message.dkim_headers.reverse();
320- message.phase = AuthPhase::Dkim;
321- } else if !message.arc_sets.is_empty() && message.arc_result == AuthResult::None {
322- message.phase = AuthPhase::Ams;
323- }
324-
325 message.into()
326 }
327 }
328 diff --git a/src/common/mod.rs b/src/common/mod.rs
329index 930c63f..f02822d 100644
330--- a/src/common/mod.rs
331+++ b/src/common/mod.rs
332 @@ -8,33 +8,26 @@ use crate::{
333 use self::headers::Header;
334
335 pub mod headers;
336+ pub mod lru;
337 pub mod message;
338 pub mod parse;
339+ pub mod resolver;
340 pub mod verify;
341
342 #[derive(Debug, Clone)]
343 pub struct AuthenticatedMessage<'x> {
344 pub(crate) headers: Vec<(&'x [u8], &'x [u8])>,
345 pub(crate) from: Vec<Cow<'x, str>>,
346- pub(crate) dkim_headers: Vec<Header<'x, dkim::Signature<'x>>>,
347- pub(crate) arc_sets: Vec<Set<'x>>,
348- pub(crate) arc_result: AuthResult<'x, ()>,
349- pub(crate) dkim_result: AuthResult<'x, dkim::Signature<'x>>,
350- pub(crate) phase: AuthPhase,
351+ pub(crate) dkim_pass: Vec<Header<'x, dkim::Signature<'x>>>,
352+ pub(crate) dkim_fail: Vec<Header<'x, crate::Error>>,
353+ pub(crate) arc_pass: Vec<Set<'x>>,
354+ pub(crate) arc_fail: Vec<Header<'x, crate::Error>>,
355 }
356
357 #[derive(Debug, PartialEq, Eq, Clone)]
358- pub enum AuthPhase {
359- Dkim,
360- Ams,
361- As(usize),
362- Done,
363- }
364-
365- #[derive(Debug, PartialEq, Eq, Clone)]
366- pub enum AuthResult<'x, T> {
367+ pub enum AuthResult {
368 None,
369- PermFail(Header<'x, crate::Error>),
370- TempFail(Header<'x, crate::Error>),
371- Pass(T),
372+ PermFail(crate::Error),
373+ TempFail(crate::Error),
374+ Pass,
375 }
376 diff --git a/src/common/parse.rs b/src/common/parse.rs
377index a2eda3a..24c0e63 100644
378--- a/src/common/parse.rs
379+++ b/src/common/parse.rs
380 @@ -2,26 +2,32 @@ use std::slice::Iter;
381
382 use mail_parser::decoders::quoted_printable::quoted_printable_decode_char;
383
384- pub(crate) const V: u16 = b'v' as u16;
385- pub(crate) const A: u16 = b'a' as u16;
386- pub(crate) const B: u16 = b'b' as u16;
387- pub(crate) const BH: u16 = (b'b' as u16) | ((b'h' as u16) << 8);
388- pub(crate) const C: u16 = b'c' as u16;
389- pub(crate) const D: u16 = b'd' as u16;
390- pub(crate) const H: u16 = b'h' as u16;
391- pub(crate) const I: u16 = b'i' as u16;
392- pub(crate) const K: u16 = b'k' as u16;
393- pub(crate) const L: u16 = b'l' as u16;
394- pub(crate) const P: u16 = b'p' as u16;
395- pub(crate) const S: u16 = b's' as u16;
396- pub(crate) const T: u16 = b't' as u16;
397- pub(crate) const X: u16 = b'x' as u16;
398- pub(crate) const Z: u16 = b'z' as u16;
399+ pub(crate) const V: u64 = b'v' as u64;
400+ pub(crate) const A: u64 = b'a' as u64;
401+ pub(crate) const B: u64 = b'b' as u64;
402+ pub(crate) const BH: u64 = (b'b' as u64) | ((b'h' as u64) << 8);
403+ pub(crate) const C: u64 = b'c' as u64;
404+ pub(crate) const D: u64 = b'd' as u64;
405+ pub(crate) const H: u64 = b'h' as u64;
406+ pub(crate) const I: u64 = b'i' as u64;
407+ pub(crate) const K: u64 = b'k' as u64;
408+ pub(crate) const L: u64 = b'l' as u64;
409+ pub(crate) const P: u64 = b'p' as u64;
410+ pub(crate) const R: u64 = b'r' as u64;
411+ pub(crate) const S: u64 = b's' as u64;
412+ pub(crate) const T: u64 = b't' as u64;
413+ pub(crate) const X: u64 = b'x' as u64;
414+ pub(crate) const Y: u64 = b'y' as u64;
415+ pub(crate) const Z: u64 = b'z' as u64;
416+
417+ pub(crate) trait TxtRecordParser: Sized {
418+ fn parse(record: &[u8]) -> crate::Result<Self>;
419+ }
420
421 pub(crate) trait TagParser: Sized {
422 fn match_bytes(&mut self, bytes: &[u8]) -> bool;
423- fn key(&mut self) -> Option<u16>;
424- fn long_key(&mut self) -> Option<u64>;
425+ fn key(&mut self) -> Option<u64>;
426+ fn value(&mut self) -> u64;
427 fn tag(&mut self) -> Vec<u8>;
428 fn tag_qp(&mut self) -> Vec<u8>;
429 fn headers_qp(&mut self) -> Vec<Vec<u8>>;
430 @@ -39,30 +45,30 @@ pub(crate) trait ItemParser: Sized {
431
432 impl TagParser for Iter<'_, u8> {
433 #[allow(clippy::while_let_on_iterator)]
434- fn key(&mut self) -> Option<u16> {
435- let mut key: u16 = 0;
436+ fn key(&mut self) -> Option<u64> {
437+ let mut key: u64 = 0;
438 let mut shift = 0;
439
440 while let Some(&ch) = self.next() {
441 match ch {
442- b'a'..=b'z' if shift < 16 => {
443- key |= (ch as u16) << shift;
444+ b'a'..=b'z' if shift < 64 => {
445+ key |= (ch as u64) << shift;
446 shift += 8;
447 }
448 b' ' | b'\t' | b'\r' | b'\n' => (),
449 b'=' => {
450 return key.into();
451 }
452- b'A'..=b'Z' if shift < 16 => {
453- key |= ((ch - b'A' + b'a') as u16) << shift;
454+ b'A'..=b'Z' if shift < 64 => {
455+ key |= ((ch - b'A' + b'a') as u64) << shift;
456 shift += 8;
457 }
458 b';' => {
459 key = 0;
460 }
461 _ => {
462- key = u16::MAX;
463- shift = 16;
464+ key = u64::MAX;
465+ shift = 64;
466 }
467 }
468 }
469 @@ -71,35 +77,32 @@ impl TagParser for Iter<'_, u8> {
470 }
471
472 #[allow(clippy::while_let_on_iterator)]
473- fn long_key(&mut self) -> Option<u64> {
474- let mut key: u64 = 0;
475+ fn value(&mut self) -> u64 {
476+ let mut value: u64 = 0;
477 let mut shift = 0;
478
479 while let Some(&ch) = self.next() {
480 match ch {
481 b'a'..=b'z' if shift < 64 => {
482- key |= (ch as u64) << shift;
483+ value |= (ch as u64) << shift;
484 shift += 8;
485 }
486 b' ' | b'\t' | b'\r' | b'\n' => (),
487- b'=' => {
488- return key.into();
489- }
490 b'A'..=b'Z' if shift < 64 => {
491- key |= ((ch - b'A' + b'a') as u64) << shift;
492+ value |= ((ch - b'A' + b'a') as u64) << shift;
493 shift += 8;
494 }
495 b';' => {
496- key = 0;
497+ break;
498 }
499 _ => {
500- key = u64::MAX;
501+ value = u64::MAX;
502 shift = 64;
503 }
504 }
505 }
506
507- None
508+ value
509 }
510
511 #[inline(always)]
512 diff --git a/src/common/resolver.rs b/src/common/resolver.rs
513new file mode 100644
514index 0000000..5176c0b
515--- /dev/null
516+++ b/src/common/resolver.rs
517 @@ -0,0 +1,317 @@
518+ use std::{
519+ borrow::Cow,
520+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
521+ sync::Arc,
522+ };
523+
524+ use trust_dns_resolver::{
525+ config::{ResolverConfig, ResolverOpts},
526+ error::ResolveError,
527+ system_conf::read_system_conf,
528+ AsyncResolver,
529+ };
530+
531+ use crate::{
532+ dkim::DomainKey,
533+ dmarc::DMARC,
534+ spf::{Macro, SPF},
535+ Error, Resolver, Txt, MX,
536+ };
537+
538+ use super::{
539+ lru::{DnsCache, LruCache},
540+ parse::TxtRecordParser,
541+ };
542+
543+ impl Resolver {
544+ pub fn new_cloudflare_tls() -> Result<Self, ResolveError> {
545+ Self::new(
546+ ResolverConfig::cloudflare_tls(),
547+ ResolverOpts::default(),
548+ 128,
549+ )
550+ }
551+
552+ pub fn new_cloudflare() -> Result<Self, ResolveError> {
553+ Self::new(ResolverConfig::cloudflare(), ResolverOpts::default(), 128)
554+ }
555+
556+ pub fn new_google() -> Result<Self, ResolveError> {
557+ Self::new(ResolverConfig::google(), ResolverOpts::default(), 128)
558+ }
559+
560+ pub fn new_quad9() -> Result<Self, ResolveError> {
561+ Self::new(ResolverConfig::quad9(), ResolverOpts::default(), 128)
562+ }
563+
564+ pub fn new_quad9_tls() -> Result<Self, ResolveError> {
565+ Self::new(ResolverConfig::quad9_tls(), ResolverOpts::default(), 128)
566+ }
567+
568+ pub fn new_system_conf() -> Result<Self, ResolveError> {
569+ let (config, options) = read_system_conf()?;
570+ Self::new(config, options, 128)
571+ }
572+
573+ pub fn new(
574+ config: ResolverConfig,
575+ options: ResolverOpts,
576+ capacity: usize,
577+ ) -> Result<Self, ResolveError> {
578+ Ok(Self {
579+ resolver: AsyncResolver::tokio(config, options)?,
580+ cache_txt: LruCache::with_capacity(capacity),
581+ cache_mx: LruCache::with_capacity(capacity),
582+ cache_ipv4: LruCache::with_capacity(capacity),
583+ cache_ipv6: LruCache::with_capacity(capacity),
584+ cache_ptr: LruCache::with_capacity(capacity),
585+ })
586+ }
587+
588+ pub(crate) async fn txt_lookup<T: TxtRecordParser + Into<Txt> + UnwrapTxtRecord>(
589+ &self,
590+ key: String,
591+ ) -> crate::Result<Arc<T>> {
592+ if let Some(value) = self.cache_txt.get(&key) {
593+ return T::unwrap_txt(value);
594+ }
595+ #[cfg(test)]
596+ if !key.is_empty() {
597+ panic!("{:?} not found.", key);
598+ }
599+
600+ let txt_lookup = self.resolver.txt_lookup(&key).await?;
601+ let mut result = Err(Error::DNSFailure("Empty TXT record.".to_string()));
602+ let records = txt_lookup.as_lookup().record_iter().filter_map(|r| {
603+ let txt_data = r.data()?.as_txt()?.txt_data();
604+ match txt_data.len() {
605+ 1 => Cow::from(txt_data[0].as_ref()).into(),
606+ 0 => None,
607+ _ => {
608+ let mut entry = Vec::with_capacity(255 * txt_data.len());
609+ for data in txt_data {
610+ entry.extend_from_slice(data);
611+ }
612+ Cow::from(entry).into()
613+ }
614+ }
615+ });
616+
617+ for record in records {
618+ result = T::parse(record.as_ref());
619+ if result.is_ok() {
620+ break;
621+ }
622+ }
623+ T::unwrap_txt(
624+ self.cache_txt
625+ .insert(key, result.into(), txt_lookup.valid_until()),
626+ )
627+ }
628+
629+ pub async fn mx_lookup(&self, key: &str) -> crate::Result<Arc<Vec<MX>>> {
630+ if let Some(value) = self.cache_mx.get(key) {
631+ return Ok(value);
632+ }
633+ #[cfg(test)]
634+ if !key.is_empty() {
635+ panic!("{:?} not found.", key);
636+ }
637+
638+ let mx_lookup = self.resolver.mx_lookup(key).await?;
639+ let mut records = mx_lookup
640+ .as_lookup()
641+ .record_iter()
642+ .filter_map(|r| {
643+ let mx = r.data()?.as_mx()?;
644+ MX {
645+ exchange: mx.exchange().to_lowercase().to_string(),
646+ preference: mx.preference(),
647+ }
648+ .into()
649+ })
650+ .collect::<Vec<_>>();
651+ records.sort_unstable_by(|a, b| a.preference.cmp(&b.preference));
652+
653+ Ok(self
654+ .cache_mx
655+ .insert(key.to_string(), Arc::new(records), mx_lookup.valid_until()))
656+ }
657+
658+ pub async fn ipv4_lookup(&self, key: &str) -> crate::Result<Arc<Vec<Ipv4Addr>>> {
659+ if let Some(value) = self.cache_ipv4.get(key) {
660+ return Ok(value);
661+ }
662+ #[cfg(test)]
663+ if !key.is_empty() {
664+ panic!("{:?} not found.", key);
665+ }
666+
667+ let ipv4_lookup = self.resolver.ipv4_lookup(key).await?;
668+ let ips = ipv4_lookup
669+ .as_lookup()
670+ .record_iter()
671+ .filter_map(|r| (*r.data()?.as_a()?).into())
672+ .collect::<Vec<_>>();
673+
674+ Ok(self
675+ .cache_ipv4
676+ .insert(key.to_string(), Arc::new(ips), ipv4_lookup.valid_until()))
677+ }
678+
679+ pub async fn ipv6_lookup(&self, key: &str) -> crate::Result<Arc<Vec<Ipv6Addr>>> {
680+ if let Some(value) = self.cache_ipv6.get(key) {
681+ return Ok(value);
682+ }
683+ #[cfg(test)]
684+ if !key.is_empty() {
685+ panic!("{:?} not found.", key);
686+ }
687+
688+ let ipv6_lookup = self.resolver.ipv6_lookup(key).await?;
689+ let ips = ipv6_lookup
690+ .as_lookup()
691+ .record_iter()
692+ .filter_map(|r| (*r.data()?.as_aaaa()?).into())
693+ .collect::<Vec<_>>();
694+
695+ Ok(self
696+ .cache_ipv6
697+ .insert(key.to_string(), Arc::new(ips), ipv6_lookup.valid_until()))
698+ }
699+
700+ pub async fn ptr_lookup(&self, addr: IpAddr) -> crate::Result<Arc<String>> {
701+ if let Some(value) = self.cache_ptr.get(&addr) {
702+ return Ok(value);
703+ }
704+ let ptr_lookup = self.resolver.reverse_lookup(addr).await?;
705+ let ptr = ptr_lookup
706+ .as_lookup()
707+ .record_iter()
708+ .filter_map(|r| r.data()?.as_ptr()?.to_lowercase().to_string().into())
709+ .next()
710+ .unwrap_or_default();
711+
712+ Ok(self
713+ .cache_ptr
714+ .insert(addr, Arc::new(ptr), ptr_lookup.valid_until()))
715+ }
716+
717+ #[cfg(test)]
718+ pub(crate) fn txt_add(
719+ &self,
720+ name: String,
721+ value: impl Into<Txt>,
722+ valid_until: std::time::Instant,
723+ ) {
724+ self.cache_txt.insert(name, value.into(), valid_until);
725+ }
726+
727+ #[cfg(test)]
728+ pub(crate) fn ipv4_add(
729+ &self,
730+ name: String,
731+ value: Vec<Ipv4Addr>,
732+ valid_until: std::time::Instant,
733+ ) {
734+ self.cache_ipv4.insert(name, Arc::new(value), valid_until);
735+ }
736+
737+ #[cfg(test)]
738+ pub(crate) fn ipv6_add(
739+ &self,
740+ name: String,
741+ value: Vec<Ipv6Addr>,
742+ valid_until: std::time::Instant,
743+ ) {
744+ self.cache_ipv6.insert(name, Arc::new(value), valid_until);
745+ }
746+
747+ #[cfg(test)]
748+ pub(crate) fn mx_add(&self, name: String, value: Vec<MX>, valid_until: std::time::Instant) {
749+ self.cache_mx.insert(name, Arc::new(value), valid_until);
750+ }
751+ }
752+
753+ impl From<ResolveError> for crate::Error {
754+ fn from(err: ResolveError) -> Self {
755+ crate::Error::DNSFailure(err.to_string())
756+ }
757+ }
758+
759+ impl From<DomainKey> for Txt {
760+ fn from(v: DomainKey) -> Self {
761+ Txt::DomainKey(v.into())
762+ }
763+ }
764+
765+ impl From<SPF> for Txt {
766+ fn from(v: SPF) -> Self {
767+ Txt::SPF(v.into())
768+ }
769+ }
770+
771+ impl From<Macro> for Txt {
772+ fn from(v: Macro) -> Self {
773+ Txt::SPFMacro(v.into())
774+ }
775+ }
776+
777+ impl From<DMARC> for Txt {
778+ fn from(v: DMARC) -> Self {
779+ Txt::DMARC(v.into())
780+ }
781+ }
782+
783+ impl<T: Into<Txt>> From<crate::Result<T>> for Txt {
784+ fn from(v: crate::Result<T>) -> Self {
785+ match v {
786+ Ok(v) => v.into(),
787+ Err(err) => Txt::Error(err),
788+ }
789+ }
790+ }
791+
792+ pub(crate) trait UnwrapTxtRecord: Sized {
793+ fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>>;
794+ }
795+
796+ impl UnwrapTxtRecord for DomainKey {
797+ fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
798+ match txt {
799+ Txt::DomainKey(a) => Ok(a),
800+ Txt::Error(err) => Err(err),
801+ _ => Err(Error::Io("Invalid record type".to_string())),
802+ }
803+ }
804+ }
805+
806+ impl UnwrapTxtRecord for SPF {
807+ fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
808+ match txt {
809+ Txt::SPF(a) => Ok(a),
810+ Txt::Error(err) => Err(err),
811+ _ => Err(Error::Io("Invalid record type".to_string())),
812+ }
813+ }
814+ }
815+
816+ impl UnwrapTxtRecord for Macro {
817+ fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
818+ match txt {
819+ Txt::SPFMacro(a) => Ok(a),
820+ Txt::Error(err) => Err(err),
821+ _ => Err(Error::Io("Invalid record type".to_string())),
822+ }
823+ }
824+ }
825+
826+ impl UnwrapTxtRecord for DMARC {
827+ fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
828+ match txt {
829+ Txt::DMARC(a) => Ok(a),
830+ Txt::Error(err) => Err(err),
831+ _ => Err(Error::Io("Invalid record type".to_string())),
832+ }
833+ }
834+ }
835 diff --git a/src/common/verify.rs b/src/common/verify.rs
836index 16fa632..fcf0007 100644
837--- a/src/common/verify.rs
838+++ b/src/common/verify.rs
839 @@ -1,115 +1,122 @@
840- use std::borrow::Cow;
841-
842 use rsa::PaddingScheme;
843 use sha1::Sha1;
844 use sha2::Sha256;
845
846 use crate::{
847- dkim::{
848- self, parse::TryIntoRecord, verify::Verifier, Algorithm, Canonicalization, Flag, PublicKey,
849- Record,
850- },
851- Error,
852+ dkim::{verify::Verifier, Algorithm, Canonicalization, DomainKey, PublicKey},
853+ Error, Resolver,
854 };
855
856- use super::{headers::Header, AuthPhase, AuthResult, AuthenticatedMessage};
857+ use super::{headers::Header, AuthResult, AuthenticatedMessage};
858
859 impl<'x> AuthenticatedMessage<'x> {
860- pub fn verify(&mut self, maybe_record: impl TryIntoRecord<'x>) {
861- let maybe_record = maybe_record.try_into_record();
862+ pub async fn verify(&mut self, resolver: &Resolver) {
863+ // Validate DKIM headers
864+ let dkim_pass_len = self.dkim_pass.len();
865+ for header in std::mem::replace(&mut self.dkim_pass, Vec::with_capacity(dkim_pass_len)) {
866+ let signature = &header.header;
867+ let record = match resolver
868+ .txt_lookup::<DomainKey>(signature.domain_key())
869+ .await
870+ {
871+ Ok(record) => record,
872+ Err(err) => {
873+ self.dkim_fail
874+ .push(Header::new(header.name, header.value, err));
875+ continue;
876+ }
877+ };
878
879- match self.phase {
880- AuthPhase::Dkim => {
881- let header = self.dkim_headers.pop().unwrap();
882- let record = match maybe_record {
883- Ok(record) => record,
884- Err(err) => {
885- self.set_dkim_error(header, err);
886- return;
887- }
888- };
889- let signature = &header.header;
890+ // Enforce t=s flag
891+ if !signature.validate_auid(&record) {
892+ self.dkim_fail.push(Header::new(
893+ header.name,
894+ header.value,
895+ Error::FailedAUIDMatch,
896+ ));
897+ continue;
898+ }
899
900- // Enforce t=s flag
901- if !record.validate_auid(&signature.i, &signature.d) {
902- self.set_dkim_error(header, Error::FailedAUIDMatch);
903- return;
904+ // Hash headers
905+ let dkim_hdr_value = header.value.strip_signature();
906+ let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
907+ let hh = match signature.a {
908+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
909+ signature.ch.hash_headers::<Sha256>(headers)
910 }
911+ Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
912+ }
913+ .unwrap_or_default();
914
915- // Hash headers
916- let dkim_hdr_value = header.value.strip_signature();
917- let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
918- let hh = match signature.a {
919- Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
920- signature.ch.hash_headers::<Sha256>(headers)
921- }
922- Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
923+ // Verify signature
924+ match signature.verify(record.as_ref(), &hh) {
925+ Ok(_) => {
926+ self.dkim_pass.push(header);
927 }
928- .unwrap_or_default();
929-
930- // Verify signature
931- match signature.verify(record.as_ref(), &hh) {
932- Ok(_) => {
933- self.dkim_result = AuthResult::Pass(header.header);
934- self.phase = if !self.arc_sets.is_empty() {
935- AuthPhase::Ams
936- } else {
937- AuthPhase::Done
938- };
939- }
940- Err(err) => {
941- self.set_dkim_error(header, err);
942- }
943+ Err(err) => {
944+ self.dkim_fail
945+ .push(Header::new(header.name, header.value, err));
946 }
947 }
948- AuthPhase::Ams => {
949- let header = &self.arc_sets.last().unwrap().signature;
950- let record = match maybe_record {
951- Ok(record) => record,
952- Err(err) => {
953- self.set_arc_error(header.name, header.value, err);
954- return;
955- }
956- };
957- let signature = &header.header;
958+ }
959
960- // Hash headers
961- let dkim_hdr_value = header.value.strip_signature();
962- let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
963- let hh = match signature.a {
964- Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
965- signature.ch.hash_headers::<Sha256>(headers)
966- }
967- Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
968+ // Validate ARC Chain
969+ if let Some(arc_set) = self.arc_pass.last() {
970+ let header = &arc_set.signature;
971+ let signature = &header.header;
972+
973+ // Hash headers
974+ let dkim_hdr_value = header.value.strip_signature();
975+ let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
976+ let hh = match signature.a {
977+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
978+ signature.ch.hash_headers::<Sha256>(headers)
979 }
980- .unwrap_or_default();
981-
982- // Verify signature
983- match signature.verify(record.as_ref(), &hh) {
984- Ok(_) => {
985- self.phase = AuthPhase::As(self.arc_sets.len() - 1);
986- }
987- Err(err) => {
988- self.set_arc_error(header.name, header.value, err);
989- }
990+ Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
991+ }
992+ .unwrap_or_default();
993+
994+ // Obtain record
995+ let record = match resolver
996+ .txt_lookup::<DomainKey>(signature.domain_key())
997+ .await
998+ {
999+ Ok(record) => record,
1000+ Err(err) => {
1001+ self.arc_fail
1002+ .push(Header::new(header.name, header.value, err));
1003+ self.arc_pass.clear();
1004+ return;
1005 }
1006+ };
1007+
1008+ // Verify signature
1009+ if let Err(err) = signature.verify(record.as_ref(), &hh) {
1010+ self.arc_fail
1011+ .push(Header::new(header.name, header.value, err));
1012+ self.arc_pass.clear();
1013+ return;
1014 }
1015- AuthPhase::As(pos) => {
1016- let header = &self.arc_sets[pos].seal;
1017- let record = match maybe_record {
1018+
1019+ // Validate ARC Seals
1020+ for (pos, set) in self.arc_pass.iter().enumerate().rev() {
1021+ // Obtain record
1022+ let header = &set.seal;
1023+ let seal = &header.header;
1024+ let record = match resolver.txt_lookup::<DomainKey>(seal.domain_key()).await {
1025 Ok(record) => record,
1026 Err(err) => {
1027- self.set_arc_error(header.name, header.value, err);
1028+ self.arc_fail
1029+ .push(Header::new(header.name, header.value, err));
1030+ self.arc_pass.clear();
1031 return;
1032 }
1033 };
1034- let seal = &header.header;
1035
1036- // Build seal headers
1037- let cur_set = &self.arc_sets[pos];
1038- let seal_signature = cur_set.seal.value.strip_signature();
1039+ // Build Seal headers
1040+ let seal_signature = header.value.strip_signature();
1041 let headers = self
1042- .arc_sets
1043+ .arc_pass
1044 .iter()
1045 .take(pos)
1046 .flat_map(|set| {
1047 @@ -120,19 +127,11 @@ impl<'x> AuthenticatedMessage<'x> {
1048 ]
1049 })
1050 .chain([
1051- (cur_set.results.name, cur_set.results.value),
1052- (cur_set.signature.name, cur_set.signature.value),
1053- (cur_set.seal.name, &seal_signature),
1054+ (set.results.name, set.results.value),
1055+ (set.signature.name, set.signature.value),
1056+ (set.seal.name, &seal_signature),
1057 ]);
1058
1059- /*let mut headers = Vec::with_capacity((pos + 1) * 3);
1060- for set in self.arc_sets.iter().take(pos + 1) {
1061- headers.push((set.results.name, Cow::from(set.results.value)));
1062- headers.push((set.signature.name, Cow::from(set.signature.value)));
1063- headers.push((set.seal.name, Cow::from(set.seal.value.strip_signature())));
1064- }
1065- let headers_iter = headers.iter().map(|(h, v)| (*h, v.as_ref()));*/
1066-
1067 let hh = match seal.a {
1068 Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1069 Canonicalization::Relaxed.hash_headers::<Sha256>(headers)
1070 @@ -141,80 +140,67 @@ impl<'x> AuthenticatedMessage<'x> {
1071 }
1072 .unwrap_or_default();
1073
1074- // Verify ARC seal
1075- match seal.verify(record.as_ref(), &hh) {
1076- Ok(_) => {
1077- if pos > 0 {
1078- self.phase = AuthPhase::As(pos - 1);
1079- } else {
1080- self.arc_result = AuthResult::Pass(());
1081- self.phase = AuthPhase::Done;
1082- }
1083- }
1084- Err(err) => {
1085- self.set_arc_error(header.name, header.value, err);
1086- }
1087+ // Verify ARC Seal
1088+ if let Err(err) = seal.verify(record.as_ref(), &hh) {
1089+ self.arc_fail
1090+ .push(Header::new(header.name, header.value, err));
1091+ self.arc_pass.clear();
1092+ return;
1093 }
1094 }
1095- AuthPhase::Done => (),
1096 }
1097 }
1098
1099- fn set_dkim_error(&mut self, header: Header<'x, dkim::Signature>, err: Error) {
1100- let header = Header::new(header.name, header.value, err);
1101- self.dkim_result = if header.header != Error::DNSFailure {
1102- AuthResult::PermFail(header)
1103- } else {
1104- AuthResult::TempFail(header)
1105- };
1106- if self.dkim_headers.is_empty() {
1107- self.phase = if !self.arc_sets.is_empty() {
1108- AuthPhase::Ams
1109+ pub fn dkim_result(&self) -> AuthResult {
1110+ if !self.dkim_pass.is_empty() {
1111+ AuthResult::Pass
1112+ } else if let Some(header) = self.dkim_fail.last() {
1113+ if matches!(header.header, Error::DNSFailure(_)) {
1114+ AuthResult::TempFail(header.header.clone())
1115 } else {
1116- AuthPhase::Done
1117- };
1118- }
1119- }
1120-
1121- fn set_arc_error(&mut self, name: &'x [u8], value: &'x [u8], err: Error) {
1122- self.arc_result = if err != Error::DNSFailure {
1123- AuthResult::PermFail(Header::new(name, value, err))
1124+ AuthResult::PermFail(header.header.clone())
1125+ }
1126 } else {
1127- AuthResult::TempFail(Header::new(name, value, err))
1128- };
1129- self.phase = AuthPhase::Done;
1130+ AuthResult::None
1131+ }
1132 }
1133
1134- pub fn next_entry(&self) -> Option<String> {
1135- let (s, d) = match self.phase {
1136- AuthPhase::Dkim => {
1137- let s = &self.dkim_headers.last().unwrap().header;
1138- (s.s.as_ref(), s.d.as_ref())
1139- }
1140- AuthPhase::Ams => {
1141- let s = &self.arc_sets.last().unwrap().signature.header;
1142- (s.s.as_ref(), s.d.as_ref())
1143- }
1144- AuthPhase::As(pos) => {
1145- let s = &self.arc_sets[pos].seal.header;
1146- (s.s.as_ref(), s.d.as_ref())
1147+ pub fn arc_result(&self) -> AuthResult {
1148+ if !self.arc_pass.is_empty() {
1149+ AuthResult::Pass
1150+ } else if let Some(header) = self.arc_fail.last() {
1151+ if matches!(header.header, Error::DNSFailure(_)) {
1152+ AuthResult::TempFail(header.header.clone())
1153+ } else {
1154+ AuthResult::PermFail(header.header.clone())
1155 }
1156- AuthPhase::Done => return None,
1157- };
1158-
1159- format!(
1160- "{}._domainkey.{}",
1161- std::str::from_utf8(s).unwrap_or_default(),
1162- std::str::from_utf8(d).unwrap_or_default()
1163- )
1164- .into()
1165+ } else {
1166+ AuthResult::None
1167+ }
1168 }
1169 }
1170
1171 pub(crate) trait VerifySignature {
1172+ fn s(&self) -> &[u8];
1173+
1174+ fn d(&self) -> &[u8];
1175+
1176 fn b(&self) -> &[u8];
1177+
1178 fn a(&self) -> Algorithm;
1179- fn verify(&self, record: &Record, hh: &[u8]) -> crate::Result<()> {
1180+
1181+ fn domain_key(&self) -> String {
1182+ let s = self.s();
1183+ let d = self.d();
1184+ let mut key = Vec::with_capacity(s.len() + d.len() + 13);
1185+ key.extend_from_slice(s);
1186+ key.extend_from_slice(b"._domainkey.");
1187+ key.extend_from_slice(d);
1188+ key.push(b'.');
1189+ String::from_utf8(key).unwrap_or_default()
1190+ }
1191+
1192+ fn verify(&self, record: &DomainKey, hh: &[u8]) -> crate::Result<()> {
1193 match (&self.a(), &record.p) {
1194 (Algorithm::RsaSha256, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
1195 public_key,
1196 @@ -247,44 +233,22 @@ pub(crate) trait VerifySignature {
1197 }
1198 }
1199
1200- impl Record {
1201- #[allow(clippy::while_let_on_iterator)]
1202- pub fn validate_auid(&self, i: &[u8], d: &[u8]) -> bool {
1203- // Enforce t=s flag
1204- if !i.is_empty() && self.has_flag(Flag::MatchDomain) {
1205- let mut auid = i.as_ref().iter();
1206- let mut domain = d.as_ref().iter();
1207- while let Some(&ch) = auid.next() {
1208- if ch == b'@' {
1209- break;
1210- }
1211- }
1212- while let Some(ch) = auid.next() {
1213- if let Some(dch) = domain.next() {
1214- if !ch.eq_ignore_ascii_case(dch) {
1215- return false;
1216- }
1217- } else {
1218- break;
1219- }
1220- }
1221- if domain.next().is_some() {
1222- return false;
1223- }
1224- }
1225-
1226- true
1227- }
1228- }
1229-
1230 #[cfg(test)]
1231 mod test {
1232- use std::{collections::HashMap, fs, path::PathBuf};
1233-
1234- use crate::common::{AuthResult, AuthenticatedMessage};
1235-
1236- #[test]
1237- fn dkim_verify() {
1238+ use std::{
1239+ fs,
1240+ path::PathBuf,
1241+ time::{Duration, Instant},
1242+ };
1243+
1244+ use crate::{
1245+ common::{parse::TxtRecordParser, AuthResult, AuthenticatedMessage},
1246+ dkim::DomainKey,
1247+ Resolver,
1248+ };
1249+
1250+ #[tokio::test]
1251+ async fn dkim_verify() {
1252 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1253 test_dir.push("resources");
1254 test_dir.push("dkim");
1255 @@ -298,58 +262,57 @@ mod test {
1256
1257 let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
1258 let (dns_records, message) = test.split_once("\n\n").unwrap();
1259- let dns_records = dns_records
1260- .split('\n')
1261- .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
1262- .collect::<HashMap<_, _>>();
1263+ let resolver = new_resolver(dns_records);
1264 let message = message.replace('\n', "\r\n");
1265
1266- let mut verifier = AuthenticatedMessage::new_(message.as_bytes(), 1667843664).unwrap();
1267- while let Some(domain) = verifier.next_entry() {
1268- verifier.verify(*dns_records.get(domain.as_str()).unwrap());
1269- }
1270- assert!(
1271- matches!(verifier.dkim_result, AuthResult::Pass(_)),
1272- "Failed: {:?}",
1273- verifier.dkim_result
1274- );
1275+ let mut verifier =
1276+ AuthenticatedMessage::parse_(message.as_bytes(), 1667843664).unwrap();
1277+ verifier.verify(&resolver).await;
1278+
1279+ assert_eq!(verifier.dkim_result(), AuthResult::Pass);
1280 }
1281 }
1282
1283- #[test]
1284- fn arc_verify() {
1285+ #[tokio::test]
1286+ async fn arc_verify() {
1287 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1288 test_dir.push("resources");
1289 test_dir.push("arc");
1290
1291 for file_name in fs::read_dir(&test_dir).unwrap() {
1292 let file_name = file_name.unwrap().path();
1293- if !file_name.to_str().unwrap().contains("002") {
1294+ /*if !file_name.to_str().unwrap().contains("002") {
1295 continue;
1296- }
1297+ }*/
1298 println!("file {}", file_name.to_str().unwrap());
1299
1300 let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
1301 let (dns_records, message) = test.split_once("\n\n").unwrap();
1302- let dns_records = dns_records
1303- .split('\n')
1304- .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
1305- .collect::<HashMap<_, _>>();
1306+ let resolver = new_resolver(dns_records);
1307 let message = message.replace('\n', "\r\n");
1308
1309- let mut verifier = AuthenticatedMessage::new_(message.as_bytes(), 1667843664).unwrap();
1310- while let Some(domain) = verifier.next_entry() {
1311- verifier.verify(*dns_records.get(domain.as_str()).unwrap());
1312- }
1313+ let mut verifier =
1314+ AuthenticatedMessage::parse_(message.as_bytes(), 1667843664).unwrap();
1315+ verifier.verify(&resolver).await;
1316
1317- println!("DKIM: {:?}", verifier.dkim_result);
1318- println!("ARC: {:?}", verifier.arc_result);
1319+ assert_eq!(verifier.arc_result(), AuthResult::Pass);
1320+ assert_eq!(verifier.dkim_result(), AuthResult::Pass);
1321+ }
1322+ }
1323
1324- /*assert!(
1325- matches!(verifier.dkim_result, AuthResult::Pass(_)),
1326- "Failed: {:?}",
1327- verifier.dkim_result
1328- );*/
1329+ fn new_resolver(dns_records: &str) -> Resolver {
1330+ let resolver = Resolver::new_system_conf().unwrap();
1331+ for (key, value) in dns_records
1332+ .split('\n')
1333+ .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
1334+ {
1335+ resolver.txt_add(
1336+ format!("{}.", key),
1337+ DomainKey::parse(value).unwrap(),
1338+ Instant::now() + Duration::new(3200, 0),
1339+ );
1340 }
1341+
1342+ resolver
1343 }
1344 }
1345 diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs
1346index 9e14ffc..2db62c1 100644
1347--- a/src/dkim/mod.rs
1348+++ b/src/dkim/mod.rs
1349 @@ -67,12 +67,20 @@ pub struct Signature<'x> {
1350 pub(crate) l: u64,
1351 pub(crate) x: u64,
1352 pub(crate) t: u64,
1353+ pub(crate) r: bool, // RFC 6651
1354+ pub(crate) atps: Option<Atps<'x>>, // RFC 6541
1355 pub(crate) ch: Canonicalization,
1356 pub(crate) cb: Canonicalization,
1357 }
1358
1359 #[derive(Debug, PartialEq, Eq, Clone)]
1360- pub struct Record {
1361+ pub struct Atps<'x> {
1362+ pub(crate) atps: Cow<'x, [u8]>,
1363+ pub(crate) atpsh: HashAlgorithm,
1364+ }
1365+
1366+ #[derive(Debug, PartialEq, Eq, Clone)]
1367+ pub struct DomainKey {
1368 pub(crate) v: Version,
1369 pub(crate) p: PublicKey,
1370 pub(crate) f: u64,
1371 @@ -153,4 +161,12 @@ impl<'x> VerifySignature for Signature<'x> {
1372 fn a(&self) -> Algorithm {
1373 self.a
1374 }
1375+
1376+ fn s(&self) -> &[u8] {
1377+ &self.s
1378+ }
1379+
1380+ fn d(&self) -> &[u8] {
1381+ &self.d
1382+ }
1383 }
1384 diff --git a/src/dkim/parse.rs b/src/dkim/parse.rs
1385index f864bcd..5b22455 100644
1386--- a/src/dkim/parse.rs
1387+++ b/src/dkim/parse.rs
1388 @@ -1,4 +1,4 @@
1389- use std::{borrow::Cow, slice::Iter};
1390+ use std::slice::Iter;
1391
1392 use mail_parser::decoders::base64::base64_decode_stream;
1393 use rsa::RsaPublicKey;
1394 @@ -6,10 +6,24 @@ use rsa::RsaPublicKey;
1395 use crate::{common::parse::*, Error};
1396
1397 use super::{
1398- Algorithm, Canonicalization, Flag, HashAlgorithm, PublicKey, Record, Service, Signature,
1399- Version,
1400+ Algorithm, Atps, Canonicalization, DomainKey, Flag, HashAlgorithm, PublicKey, Service,
1401+ Signature, Version,
1402 };
1403
1404+ const ATPSH: u64 = (b'a' as u64)
1405+ | (b't' as u64) << 8
1406+ | (b'p' as u64) << 16
1407+ | (b's' as u64) << 24
1408+ | (b'h' as u64) << 32;
1409+ const ATPS: u64 = (b'a' as u64) | (b't' as u64) << 8 | (b'p' as u64) << 16 | (b's' as u64) << 24;
1410+ const SHA256: u64 = (b's' as u64)
1411+ | (b'h' as u64) << 8
1412+ | (b'a' as u64) << 16
1413+ | (b'2' as u64) << 24
1414+ | (b'5' as u64) << 32
1415+ | (b'6' as u64) << 40;
1416+ const SHA1: u64 = (b's' as u64) | (b'h' as u64) << 8 | (b'a' as u64) << 16 | (b'1' as u64) << 24;
1417+
1418 impl<'x> Signature<'x> {
1419 #[allow(clippy::while_let_on_iterator)]
1420 pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
1421 @@ -28,6 +42,8 @@ impl<'x> Signature<'x> {
1422 t: 0,
1423 ch: Canonicalization::Simple,
1424 cb: Canonicalization::Simple,
1425+ r: false,
1426+ atps: None,
1427 };
1428 let header_len = header.len();
1429 let mut header = header.iter();
1430 @@ -64,6 +80,30 @@ impl<'x> Signature<'x> {
1431 T => signature.t = header.number().unwrap_or(0),
1432 X => signature.x = header.number().unwrap_or(0),
1433 Z => signature.z = header.headers_qp(),
1434+ R => signature.r = header.value() == Y,
1435+ ATPS => {
1436+ signature
1437+ .atps
1438+ .get_or_insert_with(|| Atps {
1439+ atps: (b""[..]).into(),
1440+ atpsh: HashAlgorithm::Sha256,
1441+ })
1442+ .atps = header.tag().into()
1443+ }
1444+ ATPSH => {
1445+ let atpsh = match header.value() {
1446+ SHA256 => HashAlgorithm::Sha256,
1447+ SHA1 => HashAlgorithm::Sha1,
1448+ _ => continue,
1449+ };
1450+ signature
1451+ .atps
1452+ .get_or_insert_with(|| Atps {
1453+ atps: (b""[..]).into(),
1454+ atpsh,
1455+ })
1456+ .atpsh = atpsh;
1457+ }
1458 _ => header.ignore(),
1459 }
1460 }
1461 @@ -193,12 +233,12 @@ enum KeyType {
1462 None,
1463 }
1464
1465- impl Record {
1466+ impl TxtRecordParser for DomainKey {
1467 #[allow(clippy::while_let_on_iterator)]
1468- pub fn parse(header: &[u8]) -> crate::Result<Self> {
1469+ fn parse(header: &[u8]) -> crate::Result<Self> {
1470 let header_len = header.len();
1471 let mut header = header.iter();
1472- let mut record = Record {
1473+ let mut record = DomainKey {
1474 v: Version::Dkim1,
1475 p: PublicKey::Revoked,
1476 f: 0,
1477 @@ -210,7 +250,7 @@ impl Record {
1478 match key {
1479 V => {
1480 if !header.match_bytes(b"DKIM1") || !header.seek_tag_end() {
1481- return Err(Error::UnsupportedRecordVersion);
1482+ return Err(Error::InvalidVersion);
1483 }
1484 }
1485 H => record.f |= header.flags::<HashAlgorithm>(),
1486 @@ -266,61 +306,14 @@ impl Record {
1487
1488 Ok(record)
1489 }
1490+ }
1491
1492+ impl DomainKey {
1493 pub fn has_flag(&self, flag: impl Into<u64>) -> bool {
1494 (self.f & flag.into()) != 0
1495 }
1496 }
1497
1498- pub trait TryIntoRecord<'x>: Sized {
1499- fn try_into_record(self) -> crate::Result<Cow<'x, Record>>;
1500- }
1501-
1502- impl<'x> TryIntoRecord<'x> for Record {
1503- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1504- Ok(Cow::Owned(self))
1505- }
1506- }
1507-
1508- impl<'x> TryIntoRecord<'x> for &'x Record {
1509- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1510- Ok(Cow::Borrowed(self))
1511- }
1512- }
1513-
1514- impl<'x> TryIntoRecord<'x> for String {
1515- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1516- Record::parse(self.as_bytes()).map(Cow::Owned)
1517- }
1518- }
1519-
1520- impl<'x> TryIntoRecord<'x> for &str {
1521- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1522- Record::parse(self.as_bytes()).map(Cow::Owned)
1523- }
1524- }
1525-
1526- impl<'x> TryIntoRecord<'x> for &[u8] {
1527- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1528- Record::parse(self).map(Cow::Owned)
1529- }
1530- }
1531-
1532- impl<'x> TryIntoRecord<'x> for Vec<u8> {
1533- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1534- Record::parse(&self).map(Cow::Owned)
1535- }
1536- }
1537-
1538- impl<'x, T: TryIntoRecord<'x> + Sized> TryIntoRecord<'x> for Option<T> {
1539- fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1540- match self {
1541- Some(v) => v.try_into_record(),
1542- None => Err(Error::DNSFailure),
1543- }
1544- }
1545- }
1546-
1547 impl ItemParser for HashAlgorithm {
1548 fn parse(bytes: &[u8]) -> Option<Self> {
1549 if bytes.eq_ignore_ascii_case(b"sha256") {
1550 @@ -362,9 +355,13 @@ mod test {
1551 use mail_parser::decoders::base64::base64_decode;
1552 use rsa::{pkcs8::DecodePublicKey, RsaPublicKey};
1553
1554- use crate::dkim::{
1555- Algorithm, Canonicalization, PublicKey, Record, Signature, Version, R_FLAG_MATCH_DOMAIN,
1556- R_FLAG_TESTING, R_HASH_SHA1, R_HASH_SHA256, R_SVC_ALL, R_SVC_EMAIL,
1557+ use crate::{
1558+ common::parse::TxtRecordParser,
1559+ dkim::{
1560+ Algorithm, Canonicalization, DomainKey, PublicKey, Signature, Version,
1561+ R_FLAG_MATCH_DOMAIN, R_FLAG_TESTING, R_HASH_SHA1, R_HASH_SHA256, R_SVC_ALL,
1562+ R_SVC_EMAIL,
1563+ },
1564 };
1565
1566 #[test]
1567 @@ -402,6 +399,8 @@ mod test {
1568 t: 311923920,
1569 ch: Canonicalization::Relaxed,
1570 cb: Canonicalization::Relaxed,
1571+ r: false,
1572+ atps: None,
1573 },
1574 ),
1575 (
1576 @@ -447,6 +446,8 @@ mod test {
1577 t: 1117574938,
1578 ch: Canonicalization::Simple,
1579 cb: Canonicalization::Simple,
1580+ r: false,
1581+ atps: None,
1582 },
1583 ),
1584 (
1585 @@ -492,6 +493,8 @@ mod test {
1586 t: 0,
1587 ch: Canonicalization::Simple,
1588 cb: Canonicalization::Relaxed,
1589+ r: false,
1590+ atps: None,
1591 },
1592 ),
1593 ] {
1594 @@ -524,7 +527,7 @@ mod test {
1595 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
1596 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
1597 ),
1598- Record {
1599+ DomainKey {
1600 v: Version::Dkim1,
1601 p: PublicKey::Rsa(
1602 RsaPublicKey::from_public_key_der(
1603 @@ -558,7 +561,7 @@ mod test {
1604 "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;",
1605 "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes "
1606 ),
1607- Record {
1608+ DomainKey {
1609 v: Version::Dkim1,
1610 p: PublicKey::Rsa(
1611 RsaPublicKey::from_public_key_der(
1612 @@ -595,7 +598,7 @@ mod test {
1613 "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
1614 "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
1615 ),
1616- Record {
1617+ DomainKey {
1618 v: Version::Dkim1,
1619 p: PublicKey::Rsa(
1620 RsaPublicKey::from_public_key_der(
1621 @@ -616,7 +619,10 @@ mod test {
1622 },
1623 ),
1624 ] {
1625- assert_eq!(Record::parse(record.as_bytes()).unwrap(), expected_result);
1626+ assert_eq!(
1627+ DomainKey::parse(record.as_bytes()).unwrap(),
1628+ expected_result
1629+ );
1630 }
1631 }
1632 }
1633 diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs
1634index 06a337b..0598caa 100644
1635--- a/src/dkim/sign.rs
1636+++ b/src/dkim/sign.rs
1637 @@ -24,7 +24,7 @@ use sha2::{Digest, Sha256};
1638
1639 use crate::Error;
1640
1641- use super::{Algorithm, Canonicalization, DKIMSigner, PrivateKey, Signature};
1642+ use super::{Algorithm, Canonicalization, DKIMSigner, HashAlgorithm, PrivateKey, Signature};
1643
1644 impl<'x> DKIMSigner<'x> {
1645 /// Creates a new DKIM signer from an RsaPrivateKey.
1646 @@ -193,6 +193,8 @@ impl<'x> DKIMSigner<'x> {
1647 a: self.a,
1648 z: Vec::new(),
1649 l: if self.l { body_len as u64 } else { 0 },
1650+ r: false,
1651+ atps: None,
1652 };
1653
1654 // Add signature to hash
1655 @@ -241,6 +243,19 @@ impl<'x> Signature<'x> {
1656 writer.write_all(b"/")?;
1657 self.cb.serialize_name(&mut writer)?;
1658
1659+ if let Some(atps) = &self.atps {
1660+ writer.write_all(b"; atps=")?;
1661+ writer.write_all(&atps.atps)?;
1662+ writer.write_all(b"; atpsh=")?;
1663+ writer.write_all(match atps.atpsh {
1664+ HashAlgorithm::Sha256 => b"sha256",
1665+ HashAlgorithm::Sha1 => b"sha1",
1666+ })?;
1667+ }
1668+ if self.r {
1669+ writer.write_all(b"; r=y")?;
1670+ }
1671+
1672 writer.write_all(b";")?;
1673 writer.write_all(new_line)?;
1674
1675 @@ -349,12 +364,15 @@ impl<'x> Display for Signature<'x> {
1676
1677 #[cfg(test)]
1678 mod test {
1679+ use std::time::{Duration, Instant};
1680+
1681 use mail_parser::decoders::base64::base64_decode;
1682 use sha2::Sha256;
1683
1684 use crate::{
1685- common::{AuthResult, AuthenticatedMessage},
1686- dkim::{Canonicalization, Signature},
1687+ common::{parse::TxtRecordParser, AuthResult, AuthenticatedMessage},
1688+ dkim::{Canonicalization, DomainKey, Signature},
1689+ Resolver,
1690 };
1691
1692 const RSA_PRIVATE_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY-----
1693 @@ -419,8 +437,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1694 );
1695 }
1696
1697- #[test]
1698- fn dkim_sign_verify() {
1699+ #[tokio::test]
1700+ async fn dkim_sign_verify() {
1701 let message = concat!(
1702 "From: bill@example.com\r\n",
1703 "To: jdoe@example.com\r\n",
1704 @@ -457,7 +475,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1705 message,
1706 RSA_PUBLIC_KEY,
1707 Ok(()),
1708- );
1709+ )
1710+ .await;
1711
1712 // Test ED25519-SHA256 relaxed/relaxed
1713 verify(
1714 @@ -476,7 +495,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1715 message,
1716 ED25519_PUBLIC_KEY,
1717 Ok(()),
1718- );
1719+ )
1720+ .await;
1721
1722 // Test RSA-SHA256 simple/simple with duplicated headers
1723 verify(
1724 @@ -499,7 +519,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1725 message_multiheader,
1726 RSA_PUBLIC_KEY,
1727 Ok(()),
1728- );
1729+ )
1730+ .await;
1731
1732 // Test RSA-SHA256 simple/relaxed with fixed body length
1733 verify(
1734 @@ -516,7 +537,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1735 &(message.to_string() + "\r\n----- Mailing list"),
1736 RSA_PUBLIC_KEY,
1737 Ok(()),
1738- );
1739+ )
1740+ .await;
1741
1742 // Test AUID not matching domain
1743 verify(
1744 @@ -532,7 +554,8 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1745 message,
1746 RSA_PUBLIC_KEY,
1747 Err(super::Error::FailedAUIDMatch),
1748- );
1749+ )
1750+ .await;
1751
1752 // Test expired signature
1753 verify(
1754 @@ -548,11 +571,12 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1755 message,
1756 RSA_PUBLIC_KEY,
1757 Err(super::Error::SignatureExpired),
1758- );
1759+ )
1760+ .await;
1761 }
1762
1763- fn verify(
1764- signature: Signature,
1765+ async fn verify(
1766+ signature: Signature<'_>,
1767 message_: &str,
1768 public_key: &str,
1769 expect: Result<(), super::Error>,
1770 @@ -562,14 +586,18 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
1771 message.extend_from_slice(message_.as_bytes());
1772 //println!("[{}]", String::from_utf8_lossy(&message));
1773
1774- let mut verifier = AuthenticatedMessage::new(&message).unwrap();
1775- while verifier.next_entry().is_some() {
1776- verifier.verify(public_key);
1777- }
1778+ let resolver = Resolver::new_system_conf().unwrap();
1779+ resolver.txt_add(
1780+ "default._domainkey.example.com.".to_string(),
1781+ DomainKey::parse(public_key.as_bytes()).unwrap(),
1782+ Instant::now() + Duration::new(3600, 0),
1783+ );
1784+ let mut verifier = AuthenticatedMessage::parse(&message).unwrap();
1785+ verifier.verify(&resolver).await;
1786
1787- match (verifier.dkim_result, &expect) {
1788- (AuthResult::Pass(_), Ok(_)) => (),
1789- (AuthResult::PermFail(hdr), Err(err)) if &hdr.header == err => (),
1790+ match (verifier.dkim_result(), &expect) {
1791+ (AuthResult::Pass, Ok(_)) => (),
1792+ (AuthResult::PermFail(hdr), Err(err)) if &hdr == err => (),
1793 (result, expect) => panic!("Expected {:?} but got {:?}.", expect, result),
1794 }
1795 }
1796 diff --git a/src/dkim/verify.rs b/src/dkim/verify.rs
1797index 19fb50f..842863a 100644
1798--- a/src/dkim/verify.rs
1799+++ b/src/dkim/verify.rs
1800 @@ -1,5 +1,7 @@
1801 use crate::common::AuthenticatedMessage;
1802
1803+ use super::{DomainKey, Flag, Signature};
1804+
1805 impl<'x> AuthenticatedMessage<'x> {
1806 pub fn signed_headers<'z: 'x>(
1807 &'z self,
1808 @@ -39,6 +41,36 @@ impl<'x> AuthenticatedMessage<'x> {
1809 }
1810 }
1811
1812+ impl<'x> Signature<'x> {
1813+ #[allow(clippy::while_let_on_iterator)]
1814+ pub fn validate_auid(&self, record: &DomainKey) -> bool {
1815+ // Enforce t=s flag
1816+ if !self.i.is_empty() && record.has_flag(Flag::MatchDomain) {
1817+ let mut auid = self.i.as_ref().iter();
1818+ let mut domain = self.d.as_ref().iter();
1819+ while let Some(&ch) = auid.next() {
1820+ if ch == b'@' {
1821+ break;
1822+ }
1823+ }
1824+ while let Some(ch) = auid.next() {
1825+ if let Some(dch) = domain.next() {
1826+ if !ch.eq_ignore_ascii_case(dch) {
1827+ return false;
1828+ }
1829+ } else {
1830+ break;
1831+ }
1832+ }
1833+ if domain.next().is_some() {
1834+ return false;
1835+ }
1836+ }
1837+
1838+ true
1839+ }
1840+ }
1841+
1842 pub(crate) trait Verifier: Sized {
1843 fn strip_signature(&self) -> Vec<u8>;
1844 }
1845 diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs
1846index cf0d610..c2ad792 100644
1847--- a/src/dmarc/mod.rs
1848+++ b/src/dmarc/mod.rs
1849 @@ -1,7 +1,7 @@
1850 pub mod parse;
1851
1852 #[derive(Debug, Clone, PartialEq, Eq)]
1853- pub(crate) struct DMARC {
1854+ pub struct DMARC {
1855 adkim: Alignment,
1856 aspf: Alignment,
1857 fo: Report,
1858 @@ -16,6 +16,7 @@ pub(crate) struct DMARC {
1859 }
1860
1861 #[derive(Debug, Clone, PartialEq, Eq)]
1862+ #[allow(clippy::upper_case_acronyms)]
1863 pub(crate) struct URI {
1864 uri: Vec<u8>,
1865 max_size: usize,
1866 diff --git a/src/dmarc/parse.rs b/src/dmarc/parse.rs
1867index 0bb0d23..f9c3379 100644
1868--- a/src/dmarc/parse.rs
1869+++ b/src/dmarc/parse.rs
1870 @@ -3,14 +3,14 @@ use std::slice::Iter;
1871 use mail_parser::decoders::quoted_printable::quoted_printable_decode_char;
1872
1873 use crate::{
1874- common::parse::{ItemParser, TagParser, V},
1875+ common::parse::{ItemParser, TagParser, TxtRecordParser, V},
1876 Error,
1877 };
1878
1879 use super::{Alignment, Format, Policy, Report, DMARC, URI};
1880
1881- impl DMARC {
1882- pub fn parse(bytes: &[u8]) -> crate::Result<Self> {
1883+ impl TxtRecordParser for DMARC {
1884+ fn parse(bytes: &[u8]) -> crate::Result<Self> {
1885 let mut record = bytes.iter();
1886 if record.key().unwrap_or(0) != V {
1887 return Err(Error::InvalidRecord);
1888 @@ -32,7 +32,7 @@ impl DMARC {
1889 sp: Policy::Unspecified,
1890 };
1891
1892- while let Some(key) = record.long_key() {
1893+ while let Some(key) = record.key() {
1894 match key {
1895 ADKIM => {
1896 dmarc.adkim = record.alignment()?;
1897 @@ -271,7 +271,10 @@ const SP: u64 = (b's' as u64) | (b'p' as u64) << 8;
1898
1899 #[cfg(test)]
1900 mod test {
1901- use crate::dmarc::{Alignment, Format, Policy, Report, DMARC, URI};
1902+ use crate::{
1903+ common::parse::TxtRecordParser,
1904+ dmarc::{Alignment, Format, Policy, Report, DMARC, URI},
1905+ };
1906
1907 #[test]
1908 fn parse_dmarc() {
1909 diff --git a/src/lib.rs b/src/lib.rs
1910index 61f73a9..eebd0d6 100644
1911--- a/src/lib.rs
1912+++ b/src/lib.rs
1913 @@ -9,7 +9,17 @@
1914 * except according to those terms.
1915 */
1916
1917- use std::fmt::Display;
1918+ use std::{
1919+ fmt::Display,
1920+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
1921+ sync::Arc,
1922+ };
1923+
1924+ use common::lru::LruCache;
1925+ use dkim::DomainKey;
1926+ use dmarc::DMARC;
1927+ use spf::{Macro, SPF};
1928+ use trust_dns_resolver::TokioAsyncResolver;
1929
1930 pub mod arc;
1931 pub mod common;
1932 @@ -17,6 +27,31 @@ pub mod dkim;
1933 pub mod dmarc;
1934 pub mod spf;
1935
1936+ #[derive(Debug)]
1937+ pub struct Resolver {
1938+ pub(crate) resolver: TokioAsyncResolver,
1939+ pub(crate) cache_txt: LruCache<String, Txt>,
1940+ pub(crate) cache_mx: LruCache<String, Arc<Vec<MX>>>,
1941+ pub(crate) cache_ipv4: LruCache<String, Arc<Vec<Ipv4Addr>>>,
1942+ pub(crate) cache_ipv6: LruCache<String, Arc<Vec<Ipv6Addr>>>,
1943+ pub(crate) cache_ptr: LruCache<IpAddr, Arc<String>>,
1944+ }
1945+
1946+ #[derive(Debug, Clone, PartialEq, Eq)]
1947+ pub(crate) enum Txt {
1948+ SPF(Arc<SPF>),
1949+ SPFMacro(Arc<Macro>),
1950+ DomainKey(Arc<DomainKey>),
1951+ DMARC(Arc<DMARC>),
1952+ Error(Error),
1953+ }
1954+
1955+ #[derive(Debug, Clone, PartialEq, Eq)]
1956+ pub struct MX {
1957+ exchange: String,
1958+ preference: u16,
1959+ }
1960+
1961 #[derive(Debug, Clone, PartialEq, Eq)]
1962 pub enum Error {
1963 ParseError,
1964 @@ -28,7 +63,6 @@ pub enum Error {
1965 UnsupportedVersion,
1966 UnsupportedAlgorithm,
1967 UnsupportedCanonicalization,
1968- UnsupportedRecordVersion,
1969 UnsupportedKeyType,
1970 FailedBodyHashMatch,
1971 RevokedPublicKey,
1972 @@ -36,7 +70,7 @@ pub enum Error {
1973 FailedVerification,
1974 SignatureExpired,
1975 FailedAUIDMatch,
1976- DNSFailure,
1977+ DNSFailure(String),
1978
1979 ARCInvalidInstance,
1980 ARCInvalidCV,
1981 @@ -67,9 +101,6 @@ impl Display for Error {
1982 Error::UnsupportedCanonicalization => {
1983 write!(f, "Unsupported canonicalization method in DKIM Signature.")
1984 }
1985- Error::UnsupportedRecordVersion => {
1986- write!(f, "Unsupported version in DKIM DNS record.")
1987- }
1988 Error::UnsupportedKeyType => {
1989 write!(f, "Unsupported key type in DKIM DNS record.")
1990 }
1991 @@ -93,7 +124,7 @@ impl Display for Error {
1992 Error::InvalidIp4 => write!(f, "Invalid IPv4."),
1993 Error::InvalidIp6 => write!(f, "Invalid IPv6."),
1994 Error::InvalidMacro => write!(f, "Invalid SPF macro."),
1995- Error::DNSFailure => write!(f, "DNS failure."),
1996+ Error::DNSFailure(err) => write!(f, "DNS failure: {}", err),
1997 }
1998 }
1999 }
2000 @@ -116,17 +147,31 @@ impl From<ed25519_dalek::ed25519::Error> for Error {
2001 }
2002 }
2003
2004- pub fn add(left: usize, right: usize) -> usize {
2005- left + right
2006- }
2007-
2008 #[cfg(test)]
2009 mod tests {
2010- use super::*;
2011+ use trust_dns_resolver::{
2012+ config::{ResolverConfig, ResolverOpts},
2013+ AsyncResolver,
2014+ };
2015+
2016+ #[tokio::test]
2017+ async fn it_works() {
2018+ let resolver =
2019+ AsyncResolver::tokio(ResolverConfig::cloudflare_tls(), ResolverOpts::default())
2020+ .unwrap();
2021+ let c = resolver
2022+ .reverse_lookup("135.181.195.209".parse().unwrap())
2023+ .await
2024+ .unwrap();
2025
2026- #[test]
2027- fn it_works() {
2028- let result = add(2, 2);
2029- assert_eq!(result, 4);
2030+ println!(
2031+ "{:#?}",
2032+ c /*c.as_lookup().records()[0]
2033+ .data()
2034+ .unwrap()
2035+ .as_txt()
2036+ .unwrap()
2037+ .to_string()*/
2038+ );
2039 }
2040 }
2041 diff --git a/src/spf/mod.rs b/src/spf/mod.rs
2042index 3e56ad6..8d3d35f 100644
2043--- a/src/spf/mod.rs
2044+++ b/src/spf/mod.rs
2045 @@ -127,7 +127,7 @@ pub(crate) enum Macro {
2046 }
2047
2048 #[derive(Debug, PartialEq, Eq, Clone)]
2049- pub(crate) struct SPF {
2050+ pub struct SPF {
2051 version: Version,
2052 directives: Vec<Directive>,
2053 modifiers: Vec<Modifier>,
2054 diff --git a/src/spf/parse.rs b/src/spf/parse.rs
2055index 144e433..482122d 100644
2056--- a/src/spf/parse.rs
2057+++ b/src/spf/parse.rs
2058 @@ -4,14 +4,14 @@ use std::{
2059 };
2060
2061 use crate::{
2062- common::parse::{TagParser, V},
2063+ common::parse::{TagParser, TxtRecordParser, V},
2064 Error,
2065 };
2066
2067 use super::{Directive, Macro, Mechanism, Modifier, Qualifier, Variable, SPF};
2068
2069- impl SPF {
2070- pub fn parse(bytes: &[u8]) -> crate::Result<SPF> {
2071+ impl TxtRecordParser for SPF {
2072+ fn parse(bytes: &[u8]) -> crate::Result<SPF> {
2073 let mut record = bytes.iter();
2074 if !matches!(record.key(), Some(k) if k == V) {
2075 return Err(Error::InvalidRecord);
2076 @@ -623,11 +623,20 @@ impl Variable {
2077 }
2078 }
2079
2080+ impl TxtRecordParser for Macro {
2081+ fn parse(record: &[u8]) -> crate::Result<Self> {
2082+ record.iter().macro_string(false).map(|(m, _)| m)
2083+ }
2084+ }
2085+
2086 #[cfg(test)]
2087 mod test {
2088 use std::net::{Ipv4Addr, Ipv6Addr};
2089
2090- use crate::spf::{Directive, Macro, Mechanism, Modifier, Qualifier, Variable, Version, SPF};
2091+ use crate::{
2092+ common::parse::TxtRecordParser,
2093+ spf::{Directive, Macro, Mechanism, Modifier, Qualifier, Variable, Version, SPF},
2094+ };
2095
2096 use super::SPFParser;
2097