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