1use std::{
8 net::{IpAddr, Ipv4Addr, Ipv6Addr},
9 sync::Arc,
10 time::SystemTime,
11};
12
13use crate::{
14 AuthenticatedMessage, DkimOutput, DkimResult, Error, MX, MessageAuthenticator, Parameters,
15 ResolverCache, Txt,
16 common::{
17 base32::Base32Writer,
18 cache::NoCache,
19 headers::Writer,
20 verify::{DomainKey, VerifySignature},
21 },
22 is_within_pct,
23};
24
25use super::{
26 Atps, DomainKeyReport, Flag, HashAlgorithm, RR_DNS, RR_EXPIRATION, RR_OTHER, RR_SIGNATURE,
27 RR_VERIFICATION, Signature,
28};
29
30impl MessageAuthenticator {
31 #[inline(always)]
33 pub async fn verify_dkim<'x, TXT, MXX, IPV4, IPV6, PTR>(
34 &self,
35 params: impl Into<Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
36 ) -> Vec<DkimOutput<'x>>
37 where
38 TXT: ResolverCache<String, Txt> + 'x,
39 MXX: ResolverCache<String, Arc<Vec<MX>>> + 'x,
40 IPV4: ResolverCache<String, Arc<Vec<Ipv4Addr>>> + 'x,
41 IPV6: ResolverCache<String, Arc<Vec<Ipv6Addr>>> + 'x,
42 PTR: ResolverCache<IpAddr, Arc<Vec<String>>> + 'x,
43 {
44 self.verify_dkim_(
45 params.into(),
46 SystemTime::now()
47 .duration_since(SystemTime::UNIX_EPOCH)
48 .map_or(0, |d| d.as_secs()),
49 )
50 .await
51 }
52
53 pub(crate) async fn verify_dkim_<'x, TXT, MXX, IPV4, IPV6, PTR>(
54 &self,
55 params: Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>,
56 now: u64,
57 ) -> Vec<DkimOutput<'x>>
58 where
59 TXT: ResolverCache<String, Txt>,
60 MXX: ResolverCache<String, Arc<Vec<MX>>>,
61 IPV4: ResolverCache<String, Arc<Vec<Ipv4Addr>>>,
62 IPV6: ResolverCache<String, Arc<Vec<Ipv6Addr>>>,
63 PTR: ResolverCache<IpAddr, Arc<Vec<String>>>,
64 {
65 let message = params.params;
66 let mut output = Vec::with_capacity(message.dkim_headers.len());
67 let mut report_requested = false;
68
69 for header in &message.dkim_headers {
71 let signature = match &header.header {
73 Ok(signature) => {
74 if signature.r {
75 report_requested = true;
76 }
77
78 if signature.x == 0 || (signature.x > signature.t && signature.x > now) {
79 signature
80 } else {
81 output.push(
82 DkimOutput::neutral(Error::SignatureExpired).with_signature(signature),
83 );
84 continue;
85 }
86 }
87 Err(err) => {
88 output.push(DkimOutput::neutral(err.clone()));
89 continue;
90 }
91 };
92
93 let ha = HashAlgorithm::from(signature.a);
95 let bh = &message
96 .body_hashes
97 .iter()
98 .find(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
99 .unwrap()
100 .3;
101
102 if bh != &signature.bh {
103 output.push(
104 DkimOutput::neutral(Error::FailedBodyHashMatch).with_signature(signature),
105 );
106 continue;
107 }
108
109 let record = match self
111 .txt_lookup::<DomainKey>(signature.domain_key(), params.cache_txt)
112 .await
113 {
114 Ok(record) => record,
115 Err(err) => {
116 output.push(DkimOutput::dns_error(err).with_signature(signature));
117 continue;
118 }
119 };
120
121 if !signature.validate_auid(&record) {
123 output.push(DkimOutput::fail(Error::FailedAuidMatch).with_signature(signature));
124 continue;
125 }
126
127 let dkim_hdr_value = header.value.strip_signature();
129 let mut headers = message.signed_headers(&signature.h, header.name, &dkim_hdr_value);
130
131 if let Err(err) = record.verify(&mut headers, signature, signature.ch) {
133 output.push(DkimOutput::fail(err).with_signature(signature));
134 continue;
135 }
136
137 if let Some(atps) = &signature.atps {
139 let mut found = false;
140 for from in &message.from {
142 if let Some((_, domain)) = from.rsplit_once('@')
143 && domain.eq(atps)
144 {
145 found = true;
146 break;
147 }
148 }
149
150 if found {
151 let mut query_domain = match &signature.atpsh {
152 Some(algorithm) => {
153 let mut writer = Base32Writer::with_capacity(40);
154 let output = algorithm.hash(signature.d.as_bytes());
155 writer.write(output.as_ref());
156 writer.finalize()
157 }
158 None => signature.d.to_string(),
159 };
160 query_domain.push_str("._atps.");
161 query_domain.push_str(atps);
162 query_domain.push('.');
163
164 match self
165 .txt_lookup::<Atps>(query_domain, params.cache_txt)
166 .await
167 {
168 Ok(_) => {
169 output.push(DkimOutput::pass().with_atps().with_signature(signature));
171 }
172 Err(err) => {
173 output.push(
174 DkimOutput::dns_error(err)
175 .with_atps()
176 .with_signature(signature),
177 );
178 }
179 }
180 continue;
181 }
182 }
183
184 output.push(DkimOutput::pass().with_signature(signature));
186 }
187
188 if report_requested {
190 for dkim in &mut output {
191 let signature = if let Some(signature) = &dkim.signature {
193 if signature.r && dkim.result != DkimResult::Pass {
194 signature
195 } else {
196 continue;
197 }
198 } else {
199 continue;
200 };
201
202 let record = if let Ok(record) = self
204 .txt_lookup::<DomainKeyReport>(
205 format!("_report._domainkey.{}.", signature.d),
206 params.cache_txt,
207 )
208 .await
209 {
210 if is_within_pct(record.rp) {
211 record
212 } else {
213 continue;
214 }
215 } else {
216 continue;
217 };
218
219 dkim.report = match &dkim.result() {
221 DkimResult::Neutral(err)
222 | DkimResult::Fail(err)
223 | DkimResult::PermError(err)
224 | DkimResult::TempError(err) => {
225 let send_report = match err {
226 Error::CryptoError(_)
227 | Error::Io(_)
228 | Error::FailedVerification
229 | Error::FailedBodyHashMatch
230 | Error::FailedAuidMatch => (record.rr & RR_VERIFICATION) != 0,
231 Error::Base64
232 | Error::UnsupportedVersion
233 | Error::UnsupportedAlgorithm
234 | Error::UnsupportedCanonicalization
235 | Error::UnsupportedKeyType
236 | Error::IncompatibleAlgorithms => (record.rr & RR_SIGNATURE) != 0,
237 Error::SignatureExpired => (record.rr & RR_EXPIRATION) != 0,
238 Error::DnsError(_)
239 | Error::DnsRecordNotFound(_)
240 | Error::InvalidRecordType
241 | Error::ParseError
242 | Error::RevokedPublicKey => (record.rr & RR_DNS) != 0,
243 Error::MissingParameters
244 | Error::NoHeadersFound
245 | Error::ArcChainTooLong
246 | Error::ArcInvalidInstance(_)
247 | Error::ArcInvalidCV
248 | Error::ArcHasHeaderTag
249 | Error::ArcBrokenChain
250 | Error::SignatureLength
251 | Error::NotAligned => (record.rr & RR_OTHER) != 0,
252 };
253
254 if send_report {
255 format!("{}@{}", record.ra, signature.d).into()
256 } else {
257 None
258 }
259 }
260 DkimResult::None | DkimResult::Pass => None,
261 };
262 }
263 }
264
265 output
266 }
267}
268
269impl<'x> AuthenticatedMessage<'x> {
270 pub async fn get_canonicalized_header(&self) -> Result<Vec<u8>, Error> {
271 let mut data = Vec::with_capacity(256);
274 for header in &self.dkim_headers {
275 let signature = match &header.header {
277 Ok(signature) => {
278 if signature.x == 0 || (signature.x > signature.t) {
279 signature
280 } else {
281 continue;
282 }
283 }
284 Err(_err) => {
285 continue;
286 }
287 };
288
289 let dkim_hdr_value = header.value.strip_signature();
291 let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
292 signature.ch.canonicalize_headers(headers, &mut data);
293
294 return Ok(data);
295 }
296 Err(Error::FailedBodyHashMatch)
298 }
299
300 pub fn signed_headers<'z: 'x>(
301 &'z self,
302 headers: &'x [String],
303 dkim_hdr_name: &'x [u8],
304 dkim_hdr_value: &'x [u8],
305 ) -> impl Iterator<Item = (&'x [u8], &'x [u8])> {
306 let mut last_header_pos: Vec<(&[u8], usize)> = Vec::new();
307 headers
308 .iter()
309 .filter_map(move |h| {
310 let header_pos = if let Some((_, header_pos)) = last_header_pos
311 .iter_mut()
312 .find(|(lh, _)| lh.eq_ignore_ascii_case(h.as_bytes()))
313 {
314 header_pos
315 } else {
316 last_header_pos.push((h.as_bytes(), 0));
317 &mut last_header_pos.last_mut().unwrap().1
318 };
319 if let Some((last_pos, result)) = self
320 .headers
321 .iter()
322 .rev()
323 .enumerate()
324 .skip(*header_pos)
325 .find(|(_, (mh, _))| h.as_bytes().eq_ignore_ascii_case(mh))
326 {
327 *header_pos = last_pos + 1;
328 Some(*result)
329 } else {
330 *header_pos = self.headers.len();
331 None
332 }
333 })
334 .chain([(dkim_hdr_name, dkim_hdr_value)])
335 }
336}
337
338impl Signature {
339 #[allow(clippy::while_let_on_iterator)]
340 pub(crate) fn validate_auid(&self, record: &DomainKey) -> bool {
341 if !self.i.is_empty() && record.has_flag(Flag::MatchDomain) {
343 let mut auid = self.i.chars();
344 let mut domain = self.d.chars();
345 while let Some(ch) = auid.next() {
346 if ch == '@' {
347 break;
348 }
349 }
350 while let Some(ch) = auid.next() {
351 if let Some(dch) = domain.next() {
352 if ch != dch {
353 return false;
354 }
355 } else {
356 break;
357 }
358 }
359 if domain.next().is_some() {
360 return false;
361 }
362 }
363
364 true
365 }
366}
367
368pub(crate) trait Verifier: Sized {
369 fn strip_signature(&self) -> Vec<u8>;
370}
371
372impl Verifier for &[u8] {
373 fn strip_signature(&self) -> Vec<u8> {
374 let mut unsigned_dkim = Vec::with_capacity(self.len());
375 let mut iter = self.iter().enumerate();
376 let mut last_ch = b';';
377 while let Some((pos, &ch)) = iter.next() {
378 match ch {
379 b'=' if last_ch == b'b' => {
380 unsigned_dkim.push(ch);
381 #[allow(clippy::while_let_on_iterator)]
382 while let Some((_, &ch)) = iter.next() {
383 if ch == b';' {
384 unsigned_dkim.push(b';');
385 break;
386 }
387 }
388 last_ch = 0;
389 }
390 b'b' | b'B' if last_ch == b';' => {
391 last_ch = b'b';
392 unsigned_dkim.push(ch);
393 }
394 b';' => {
395 last_ch = b';';
396 unsigned_dkim.push(ch);
397 }
398 b'\r' if pos == self.len() - 2 => (),
399 b'\n' if pos == self.len() - 1 => (),
400 _ => {
401 unsigned_dkim.push(ch);
402 if !ch.is_ascii_whitespace() {
403 last_ch = 0;
404 }
405 }
406 }
407 }
408 unsigned_dkim
409 }
410}
411
412impl<'x> From<&'x AuthenticatedMessage<'x>>
413 for Parameters<
414 'x,
415 &'x AuthenticatedMessage<'x>,
416 NoCache<String, Txt>,
417 NoCache<String, Arc<Vec<MX>>>,
418 NoCache<String, Arc<Vec<Ipv4Addr>>>,
419 NoCache<String, Arc<Vec<Ipv6Addr>>>,
420 NoCache<IpAddr, Arc<Vec<String>>>,
421 >
422{
423 fn from(params: &'x AuthenticatedMessage<'x>) -> Self {
424 Parameters::new(params)
425 }
426}
427
428#[cfg(test)]
429#[allow(unused)]
430pub mod test {
431 use std::{
432 fs,
433 path::PathBuf,
434 time::{Duration, Instant},
435 };
436
437 use mail_parser::MessageParser;
438
439 use crate::{
440 AuthenticatedMessage, DkimResult, MessageAuthenticator,
441 common::{cache::test::DummyCaches, parse::TxtRecordParser, verify::DomainKey},
442 dkim::verify::Verifier,
443 };
444
445 #[tokio::test]
446 async fn dkim_verify() {
447 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
448 test_dir.push("resources");
449 test_dir.push("dkim");
450 let resolver = MessageAuthenticator::new_system_conf().unwrap();
451
452 for file_name in fs::read_dir(&test_dir).unwrap() {
453 let file_name = file_name.unwrap().path();
454 println!("DKIM verifying {}", file_name.to_str().unwrap());
458
459 let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
460 let (dns_records, raw_message) = test.split_once("\n\n").unwrap();
461 let caches = new_cache(dns_records);
462 let raw_message = raw_message.replace('\n', "\r\n");
463 let message = AuthenticatedMessage::parse(raw_message.as_bytes()).unwrap();
464 assert_eq!(
465 message,
466 AuthenticatedMessage::from_parsed(
467 &MessageParser::new().parse(&raw_message).unwrap(),
468 true
469 )
470 );
471
472 let dkim = resolver
473 .verify_dkim_(caches.parameters(&message), 1667843664)
474 .await;
475
476 assert_eq!(dkim.last().unwrap().result(), &DkimResult::Pass);
477 }
478 }
479
480 #[test]
481 fn dkim_strip_signature() {
482 for (value, stripped_value) in [
483 ("b=abc;h=From\r\n", "b=;h=From"),
484 ("bh=B64b=;h=From;b=abc\r\n", "bh=B64b=;h=From;b="),
485 ("h=From; b = abc\r\ndef\r\n; v=1\r\n", "h=From; b =; v=1"),
486 ("B\r\n=abc;v=1\r\n", "B\r\n=;v=1"),
487 ] {
488 assert_eq!(
489 String::from_utf8(value.as_bytes().strip_signature()).unwrap(),
490 stripped_value
491 );
492 }
493 }
494
495 pub(crate) fn new_cache(dns_records: &str) -> DummyCaches {
496 let caches = DummyCaches::new();
497 for (key, value) in dns_records
498 .split('\n')
499 .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
500 {
501 caches.txt_add(
502 format!("{key}."),
503 DomainKey::parse(value).unwrap(),
504 Instant::now() + Duration::new(3200, 0),
505 );
506 }
507
508 caches
509 }
510}