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