Commit
+919 -435 +/-18 browse
1 | diff --git a/Cargo.toml b/Cargo.toml |
2 | index 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 |
17 | index 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 |
49 | index 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 |
62 | new file mode 100644 |
63 | index 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 |
123 | index 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 |
329 | index 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 |
377 | index 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 |
513 | new file mode 100644 |
514 | index 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 |
836 | index 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 |
1346 | index 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 |
1385 | index 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 |
1634 | index 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 |
1797 | index 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 |
1846 | index 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 |
1867 | index 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 |
1910 | index 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 |
2042 | index 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 |
2055 | index 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 |