1use crate::SystemTime;
8use crate::{
9 AuthenticatedMessage, DkimOutput, DkimResult, Error, MX, MessageAuthenticator, Parameters,
10 RecordSet, ResolverCache, Txt,
11 common::{
12 base32::Base32Writer,
13 cache::NoCache,
14 headers::Writer,
15 verify::{DomainKey, VerifySignature},
16 },
17 is_within_pct,
18};
19use crate::{DnsError, common::crypto::CryptoError};
20use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21
22use super::{
23 Atps, DkimError, DomainKeyReport, Flag, HashAlgorithm, RR_DNS, RR_EXPIRATION, RR_OTHER,
24 RR_SIGNATURE, RR_VERIFICATION, Signature,
25};
26
27impl MessageAuthenticator {
28 #[inline(always)]
30 pub async fn verify_dkim<'x, TXT, MXX, IPV4, IPV6, PTR>(
31 &self,
32 params: impl Into<Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
33 ) -> Vec<DkimOutput<'x>>
34 where
35 TXT: ResolverCache<Box<str>, Txt> + 'x,
36 MXX: ResolverCache<Box<str>, RecordSet<MX>> + 'x,
37 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>> + 'x,
38 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>> + 'x,
39 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>> + 'x,
40 {
41 self.verify_dkim_(
42 params.into(),
43 SystemTime::now()
44 .duration_since(SystemTime::UNIX_EPOCH)
45 .map_or(0, |d| d.as_secs()),
46 )
47 .await
48 }
49
50 pub(crate) async fn verify_dkim_<'x, TXT, MXX, IPV4, IPV6, PTR>(
51 &self,
52 params: Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>,
53 now: u64,
54 ) -> Vec<DkimOutput<'x>>
55 where
56 TXT: ResolverCache<Box<str>, Txt>,
57 MXX: ResolverCache<Box<str>, RecordSet<MX>>,
58 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>>,
59 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>>,
60 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>>,
61 {
62 let message = params.params;
63 let mut output = Vec::with_capacity(message.dkim_headers.len() + message.errors.len());
64 let mut report_requested = false;
65
66 for header in &message.errors {
68 if let Error::Dkim(_) = &header.header {
69 output.push(DkimOutput::neutral(header.header.clone()));
70 }
71 }
72
73 for header in &message.dkim_headers {
75 let signature = &header.header;
76 if signature.r {
77 report_requested = true;
78 }
79
80 if !(signature.x == 0 || (signature.x > signature.t && signature.x > now)) {
81 output.push(
82 DkimOutput::neutral(Error::Dkim(DkimError::SignatureExpired))
83 .with_signature(signature),
84 );
85 continue;
86 }
87
88 let ha = HashAlgorithm::from(signature.a);
90 let bh = &message
91 .body_hashes
92 .iter()
93 .find(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
94 .unwrap()
95 .3;
96
97 if bh != &signature.bh {
98 output.push(
99 DkimOutput::neutral(Error::Dkim(DkimError::FailedBodyHashMatch))
100 .with_signature(signature),
101 );
102 continue;
103 }
104
105 let record = match self
107 .txt_lookup::<DomainKey>(signature.domain_key(), params.cache_txt)
108 .await
109 {
110 Ok(record) => record,
111 Err(err) => {
112 output.push(DkimOutput::dns_error(err).with_signature(signature));
113 continue;
114 }
115 };
116
117 if !signature.validate_auid(&record) {
119 output.push(
120 DkimOutput::fail(Error::Dkim(DkimError::FailedAuidMatch))
121 .with_signature(signature),
122 );
123 continue;
124 }
125
126 let dkim_hdr_value = header.value.strip_signature();
128 let mut headers = message.signed_headers(&signature.h, header.name, &dkim_hdr_value);
129
130 if let Err(err) = record.verify(&mut headers, signature, signature.ch) {
132 output.push(DkimOutput::fail(err).with_signature(signature));
133 continue;
134 }
135
136 if let Some(atps) = &signature.atps {
138 let mut found = false;
139 for from in &message.from {
141 if let Some((_, domain)) = from.rsplit_once('@')
142 && domain.eq(atps)
143 {
144 found = true;
145 break;
146 }
147 }
148
149 if found {
150 let mut query_domain = match &signature.atpsh {
151 Some(algorithm) => {
152 let mut writer = Base32Writer::with_capacity(40);
153 let output = algorithm.hash(signature.d.as_bytes());
154 writer.write(output.as_ref());
155 writer.finalize()
156 }
157 None => signature.d.to_string(),
158 };
159 query_domain.push_str("._atps.");
160 query_domain.push_str(atps);
161 query_domain.push('.');
162
163 match self
164 .txt_lookup::<Atps>(query_domain, params.cache_txt)
165 .await
166 {
167 Ok(_) => {
168 output.push(DkimOutput::pass().with_atps().with_signature(signature));
170 }
171 Err(err) => {
172 output.push(
173 DkimOutput::dns_error(err)
174 .with_atps()
175 .with_signature(signature),
176 );
177 }
178 }
179 continue;
180 }
181 }
182
183 output.push(DkimOutput::pass().with_signature(signature));
185 }
186
187 if report_requested {
189 for dkim in &mut output {
190 let signature = if let Some(signature) = &dkim.signature {
192 if signature.r && dkim.result != DkimResult::Pass {
193 signature
194 } else {
195 continue;
196 }
197 } else {
198 continue;
199 };
200
201 let record = if let Ok(record) = self
203 .txt_lookup::<DomainKeyReport>(
204 format!("_report._domainkey.{}.", signature.d),
205 params.cache_txt,
206 )
207 .await
208 {
209 if is_within_pct(record.rp) {
210 record
211 } else {
212 continue;
213 }
214 } else {
215 continue;
216 };
217
218 dkim.report = match &dkim.result() {
220 DkimResult::Neutral(err)
221 | DkimResult::Fail(err)
222 | DkimResult::PermError(err)
223 | DkimResult::TempError(err) => {
224 let send_report = match err {
225 Error::Crypto(CryptoError::Library(_))
226 | Error::Io(_)
227 | Error::Crypto(CryptoError::FailedVerification)
228 | Error::Dkim(DkimError::FailedBodyHashMatch)
229 | Error::Dkim(DkimError::FailedAuidMatch) => {
230 (record.rr & RR_VERIFICATION) != 0
231 }
232 Error::Base64
233 | Error::Dkim(DkimError::UnsupportedVersion)
234 | Error::Dkim(DkimError::UnsupportedAlgorithm)
235 | Error::Dkim(DkimError::UnsupportedCanonicalization)
236 | Error::Dkim(DkimError::UnsupportedKeyType)
237 | Error::Crypto(CryptoError::IncompatibleAlgorithms) => {
238 (record.rr & RR_SIGNATURE) != 0
239 }
240 Error::Dkim(DkimError::SignatureExpired) => {
241 (record.rr & RR_EXPIRATION) != 0
242 }
243 Error::Dns(DnsError::Resolver(_))
244 | Error::Dns(DnsError::RecordNotFound(_))
245 | Error::Dns(DnsError::InvalidRecordType)
246 | Error::ParseError
247 | Error::Dkim(DkimError::RevokedPublicKey) => (record.rr & RR_DNS) != 0,
248 #[cfg(feature = "arc")]
249 Error::Arc(_) => (record.rr & RR_OTHER) != 0,
250 Error::MissingParameters
251 | Error::NoHeadersFound
252 | Error::Dkim(DkimError::SignatureLength)
253 | Error::NotAligned
254 | Error::Dkim2(_) => (record.rr & RR_OTHER) != 0,
255 };
256
257 if send_report {
258 format!("{}@{}", record.ra, signature.d).into()
259 } else {
260 None
261 }
262 }
263 DkimResult::None | DkimResult::Pass => None,
264 };
265 }
266 }
267
268 output
269 }
270}
271
272impl<'x> AuthenticatedMessage<'x> {
273 pub async fn get_canonicalized_header(&self) -> Result<Vec<u8>, Error> {
274 let mut data = Vec::with_capacity(256);
277 for header in &self.dkim_headers {
278 let signature = &header.header;
280 if !(signature.x == 0 || (signature.x > signature.t)) {
281 continue;
282 }
283
284 let dkim_hdr_value = header.value.strip_signature();
286 let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
287 signature.ch.canonicalize_headers(headers, &mut data);
288
289 return Ok(data);
290 }
291 Err(Error::Dkim(DkimError::FailedBodyHashMatch))
293 }
294
295 pub fn signed_headers<'z: 'x>(
296 &'z self,
297 headers: &'x [String],
298 dkim_hdr_name: &'x [u8],
299 dkim_hdr_value: &'x [u8],
300 ) -> impl Iterator<Item = (&'x [u8], &'x [u8])> {
301 let mut last_header_pos: Vec<(&[u8], usize)> = Vec::with_capacity(headers.len());
302 headers
303 .iter()
304 .filter_map(move |h| {
305 let name = h.as_bytes();
306 let slot = match last_header_pos
307 .iter()
308 .position(|(lh, _)| lh.eq_ignore_ascii_case(name))
309 {
310 Some(slot) => slot,
311 None => {
312 last_header_pos.push((name, 0));
313 last_header_pos.len() - 1
314 }
315 };
316 let header_pos = last_header_pos.get(slot).map_or(0, |(_, pos)| *pos);
317 let (next_pos, result) = match self
318 .headers
319 .iter()
320 .rev()
321 .enumerate()
322 .skip(header_pos)
323 .find(|(_, (mh, _))| name.eq_ignore_ascii_case(mh))
324 {
325 Some((last_pos, result)) => (last_pos + 1, Some(*result)),
326 None => (self.headers.len(), None),
327 };
328 if let Some((_, pos)) = last_header_pos.get_mut(slot) {
329 *pos = next_pos;
330 }
331 result
332 })
333 .chain([(dkim_hdr_name, dkim_hdr_value)])
334 }
335}
336
337impl Signature {
338 pub(crate) fn validate_auid(&self, record: &DomainKey) -> bool {
339 if self.i.is_empty() {
340 return true;
341 }
342
343 let auid_domain = self
344 .i
345 .split_once('@')
346 .map_or("", |(_, auid_domain)| auid_domain)
347 .as_bytes();
348 let domain = self.d.as_bytes();
349
350 match auid_domain
351 .len()
352 .checked_sub(domain.len())
353 .and_then(|split| auid_domain.split_at_checked(split))
354 {
355 Some((parent, suffix)) if suffix.eq_ignore_ascii_case(domain) => {
356 parent.is_empty() || (!record.has_flag(Flag::MatchDomain) && parent.ends_with(b"."))
357 }
358 _ => false,
359 }
360 }
361}
362
363pub(crate) trait Verifier: Sized {
364 fn strip_signature(&self) -> Vec<u8>;
365}
366
367#[derive(Clone, Copy, PartialEq, Eq)]
368enum TagState {
369 Semicolon,
370 Tag,
371 Value,
372 Signature,
373}
374
375fn strip_tag_segment(
376 segment: &[u8],
377 terminated: bool,
378 state: TagState,
379 unsigned_dkim: &mut Vec<u8>,
380) -> TagState {
381 if state != TagState::Semicolon {
382 unsigned_dkim.extend_from_slice(segment);
383 return if terminated {
384 TagState::Semicolon
385 } else {
386 state
387 };
388 }
389
390 let tag = segment
391 .iter()
392 .position(|ch| !ch.is_ascii_whitespace())
393 .unwrap_or(segment.len());
394
395 if !matches!(segment.get(tag), Some(b'b' | b'B')) {
396 unsigned_dkim.extend_from_slice(segment);
397 return if terminated || tag == segment.len() {
398 TagState::Semicolon
399 } else {
400 TagState::Value
401 };
402 }
403
404 let after_tag = &segment[tag + 1..];
405 let equals = after_tag
406 .iter()
407 .position(|ch| !ch.is_ascii_whitespace())
408 .unwrap_or(after_tag.len());
409
410 if after_tag.get(equals) != Some(&b'=') {
411 unsigned_dkim.extend_from_slice(segment);
412 return if terminated {
413 TagState::Semicolon
414 } else if equals == after_tag.len() {
415 TagState::Tag
416 } else {
417 TagState::Value
418 };
419 }
420
421 unsigned_dkim.extend_from_slice(&segment[..tag + equals + 2]);
422 if terminated {
423 unsigned_dkim.push(b';');
424 TagState::Value
425 } else {
426 TagState::Signature
427 }
428}
429
430fn strip_tag_list(mut rest: &[u8], unsigned_dkim: &mut Vec<u8>) -> TagState {
431 let mut state = TagState::Semicolon;
432 loop {
433 match memchr::memchr(b';', rest) {
434 Some(position) => {
435 let (segment, tail) = rest.split_at(position + 1);
436 state = strip_tag_segment(segment, true, state, unsigned_dkim);
437 rest = tail;
438 }
439 None => return strip_tag_segment(rest, false, state, unsigned_dkim),
440 }
441 }
442}
443
444fn strip_trailing_byte(ch: u8, discard: bool, state: &mut TagState, unsigned_dkim: &mut Vec<u8>) {
445 if *state == TagState::Signature {
446 if ch == b';' {
447 unsigned_dkim.push(b';');
448 *state = TagState::Semicolon;
449 }
450 return;
451 }
452
453 match ch {
454 b'=' if *state == TagState::Tag => {
455 unsigned_dkim.push(ch);
456 *state = TagState::Signature;
457 }
458 b'b' | b'B' if *state == TagState::Semicolon => {
459 unsigned_dkim.push(ch);
460 *state = TagState::Tag;
461 }
462 b';' => {
463 unsigned_dkim.push(ch);
464 *state = TagState::Semicolon;
465 }
466 _ if discard => (),
467 _ => {
468 unsigned_dkim.push(ch);
469 if !ch.is_ascii_whitespace() {
470 *state = TagState::Value;
471 }
472 }
473 }
474}
475
476impl Verifier for &[u8] {
477 fn strip_signature(&self) -> Vec<u8> {
478 let mut unsigned_dkim = Vec::with_capacity(self.len());
479 let (head, tail) = match self.len() {
480 0 => return unsigned_dkim,
481 1 => self.split_at(0),
482 len => self.split_at(len - 2),
483 };
484
485 let mut state = strip_tag_list(head, &mut unsigned_dkim);
486 match tail {
487 [cr, lf] => {
488 strip_trailing_byte(*cr, *cr == b'\r', &mut state, &mut unsigned_dkim);
489 strip_trailing_byte(*lf, *lf == b'\n', &mut state, &mut unsigned_dkim);
490 }
491 [lf] => strip_trailing_byte(*lf, *lf == b'\n', &mut state, &mut unsigned_dkim),
492 _ => (),
493 }
494
495 unsigned_dkim
496 }
497}
498
499impl<'x> From<&'x AuthenticatedMessage<'x>>
500 for Parameters<
501 'x,
502 &'x AuthenticatedMessage<'x>,
503 NoCache<Box<str>, Txt>,
504 NoCache<Box<str>, RecordSet<MX>>,
505 NoCache<Box<str>, RecordSet<Ipv4Addr>>,
506 NoCache<Box<str>, RecordSet<Ipv6Addr>>,
507 NoCache<IpAddr, RecordSet<Box<str>>>,
508 >
509{
510 fn from(params: &'x AuthenticatedMessage<'x>) -> Self {
511 Parameters::new(params)
512 }
513}
514
515#[cfg(test)]
516#[allow(unused)]
517pub mod test {
518 use std::{
519 fs,
520 path::PathBuf,
521 time::{Duration, Instant},
522 };
523
524 use mail_parser::MessageParser;
525
526 use crate::{
527 AuthenticatedMessage, DkimResult, MessageAuthenticator,
528 common::{cache::test::DummyCaches, parse::TxtRecordParser, verify::DomainKey},
529 dkim::{Signature, verify::Verifier},
530 };
531
532 #[test]
533 fn validate_auid() {
534 let strict = DomainKey::parse(
535 b"v=DKIM1; k=ed25519; t=s; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
536 )
537 .unwrap();
538 let relaxed =
539 DomainKey::parse(b"v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=")
540 .unwrap();
541
542 for (auid, strict_expected, relaxed_expected) in [
543 ("", true, true),
544 ("@example.com", true, true),
545 ("john@example.com", true, true),
546 ("@EXAMPLE.com", true, true),
547 ("@sub.example.com", false, true),
548 ("john@deep.sub.example.com", false, true),
549 ("@example.com.evil", false, false),
550 ("@xexample.com", false, false),
551 ("@other.org", false, false),
552 ("john", false, false),
553 ("@", false, false),
554 ] {
555 let signature = Signature {
556 i: auid.to_string(),
557 d: "example.com".to_string(),
558 ..Default::default()
559 };
560 assert_eq!(
561 signature.validate_auid(&strict),
562 strict_expected,
563 "t=s {auid:?}"
564 );
565 assert_eq!(
566 signature.validate_auid(&relaxed),
567 relaxed_expected,
568 "{auid:?}"
569 );
570 }
571 }
572
573 #[tokio::test]
574 async fn dkim_verify() {
575 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
576 test_dir.push("resources");
577 test_dir.push("dkim");
578 let resolver = MessageAuthenticator::new_system_conf().unwrap();
579
580 for file_name in fs::read_dir(&test_dir).unwrap() {
581 let file_name = file_name.unwrap().path();
582 println!("DKIM verifying {}", file_name.to_str().unwrap());
586
587 let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
588 let (dns_records, raw_message) = test.split_once("\n\n").unwrap();
589 let caches = new_cache(dns_records);
590 let raw_message = raw_message.replace('\n', "\r\n");
591 let message = AuthenticatedMessage::parse(raw_message.as_bytes()).unwrap();
592 assert_eq!(
593 message,
594 AuthenticatedMessage::from_parsed(
595 &MessageParser::new().parse(&raw_message).unwrap(),
596 raw_message.as_bytes(),
597 true
598 )
599 );
600
601 let dkim = resolver
602 .verify_dkim_(caches.parameters(&message), 1667843664)
603 .await;
604
605 assert_eq!(dkim.last().unwrap().result(), &DkimResult::Pass);
606 }
607 }
608
609 #[test]
610 fn dkim_strip_signature() {
611 for (value, stripped_value) in [
612 ("b=abc;h=From\r\n", "b=;h=From"),
613 ("bh=B64b=;h=From;b=abc\r\n", "bh=B64b=;h=From;b="),
614 ("h=From; b = abc\r\ndef\r\n; v=1\r\n", "h=From; b =; v=1"),
615 ("B\r\n=abc;v=1\r\n", "B\r\n=;v=1"),
616 ] {
617 assert_eq!(
618 String::from_utf8(value.as_bytes().strip_signature()).unwrap(),
619 stripped_value
620 );
621 }
622 }
623
624 pub(crate) fn new_cache(dns_records: &str) -> DummyCaches {
625 let caches = DummyCaches::new();
626 for (key, value) in dns_records
627 .split('\n')
628 .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
629 {
630 caches.txt_add(
631 format!("{key}."),
632 DomainKey::parse(value).unwrap(),
633 Instant::now() + Duration::new(3200, 0),
634 );
635 }
636
637 caches
638 }
639}