1use super::{
8 ChainBinding, ChainLink, Dkim2Error, Dkim2Output, Flag, MessageInstance, Signature,
9 sign::Envelope,
10};
11use crate::{
12 AuthenticatedMessage, Dkim2Result, DnsError, Error, MX, MessageAuthenticator, Parameters,
13 RecordSet, ResolverCache, Txt,
14 common::{
15 crypto::{Algorithm, CryptoError, HashAlgorithm},
16 headers::{Header, HeaderIterator, HeaderStream, Writer},
17 verify::DomainKey,
18 },
19 dkim::DkimError,
20 dkim2::{canonicalize::CanonicalizedHeaderWriter, sign::now},
21};
22use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
23
24const MAX_AGE: u64 = 14 * 86400;
25const MAX_CHAIN_LENGTH: usize = 50;
26
27impl MessageAuthenticator {
28 pub async fn verify_dkim2<'x, TXT, MXX, IPV4, IPV6, PTR, A, R>(
30 &self,
31 params: impl Into<Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
32 envelope: Envelope<A, R>,
33 ) -> Dkim2Output<'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 A: AsRef<str>,
41 R: IntoIterator<Item: AsRef<str>>,
42 {
43 let params = params.into();
44 self.verify_dkim2_(params.params, envelope, params.cache_txt, now(), true)
45 .await
46 }
47
48 pub(crate) async fn verify_dkim2_<'x, TXT, A, R>(
49 &self,
50 message: &'x AuthenticatedMessage<'x>,
51 envelope: Envelope<A, R>,
52 cache_txt: Option<&TXT>,
53 now: u64,
54 body_present: bool,
55 ) -> Dkim2Output<'x>
56 where
57 TXT: ResolverCache<Box<str>, Txt>,
58 A: AsRef<str>,
59 R: IntoIterator<Item: AsRef<str>>,
60 {
61 if message.has_dkim2_errors {
62 for header in &message.errors {
63 let name = header.name.trim_ascii();
64
65 if name.eq_ignore_ascii_case(b"dkim2-signature")
66 || name.eq_ignore_ascii_case(b"message-instance")
67 {
68 return Dkim2Result::from(header.header.clone()).into();
69 }
70 }
71 }
72
73 if message.dkim2_signatures.is_empty() {
74 return Dkim2Result::None.into();
75 } else if message.dkim2_signatures.len() > MAX_CHAIN_LENGTH
76 || message.dkim2_instances.len() > MAX_CHAIN_LENGTH
77 {
78 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::ChainTooLong)).into();
79 }
80
81 let signatures = message.dkim2_signatures.as_slice();
82 let instances = message.dkim2_instances.as_slice();
83
84 for (index, header) in signatures.iter().enumerate() {
85 let signature = &header.header;
86 let expected = index as u32 + 1;
87 if signature.i != expected {
88 return Dkim2Result::None.into();
89 }
90 for (present, tag) in [(signature.m != 0, "m"), (!signature.d.is_empty(), "d")] {
91 if !present {
92 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::SignatureTagMissing {
93 i: signature.i,
94 tag,
95 }))
96 .into();
97 }
98 }
99 if let ChainBinding::Envelope { mail_from, rcpt_to } = &signature.chain {
100 if mail_from.is_empty() && rcpt_to.is_empty() {
101 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::SignatureTagMissing {
102 i: signature.i,
103 tag: "mf",
104 }))
105 .into();
106 }
107 if require_reverse_path()
108 && !(is_reverse_path(mail_from) && rcpt_to.iter().all(|r| is_reverse_path(r)))
109 {
110 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::SignatureSyntax(
111 signature.i,
112 )))
113 .into();
114 }
115 }
116 if now > signature.t && now - signature.t > MAX_AGE {
117 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::SignatureExpired(
118 signature.i,
119 )))
120 .into();
121 }
122 }
123
124 for (index, header) in instances.iter().enumerate() {
125 let instance = &header.header;
126 if instance.m != index as u32 + 1 {
127 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::InstanceMissing(
128 index as u32 + 1,
129 )))
130 .into();
131 }
132 }
133
134 let highest_sig_m = signatures.last().map(|h| h.header.m).unwrap_or(0);
135 let highest_mi_m = instances.last().map(|h| h.header.m).unwrap_or(0);
136 if highest_mi_m == 0 {
137 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::InstanceMissing(1))).into();
138 }
139 if highest_mi_m != highest_sig_m {
140 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::InstanceAboveSignature(
141 highest_mi_m,
142 )))
143 .into();
144 }
145
146 let top_signature = &signatures.last().unwrap().header;
147 match &top_signature.chain {
148 ChainBinding::Envelope { mail_from, rcpt_to } => {
149 if !address_matches(envelope.mail_from.as_ref(), mail_from) {
150 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::MailFromMismatch(
151 top_signature.i,
152 )))
153 .into();
154 }
155 for rcpt in envelope.rcpt_to {
156 if !rcpt_to.iter().any(|r| address_matches(rcpt.as_ref(), r)) {
157 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::RcptToMismatch(
158 top_signature.i,
159 )))
160 .into();
161 }
162 }
163 if mail_from != "<>" {
164 let (_, domain) = local_and_domain(mail_from);
165 if !relaxed_domain_match(domain, &top_signature.d) {
166 return Dkim2Result::PermError(Error::Dkim2(
167 Dkim2Error::MailFromDomainMismatch(top_signature.i),
168 ))
169 .into();
170 }
171 }
172 }
173 ChainBinding::NextDomain(_) => {
174 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::SignatureTagUnexpected {
175 i: top_signature.i,
176 tag: "nd",
177 }))
178 .into();
179 }
180 }
181
182 for window in signatures.windows(2) {
183 let previous = &window[0].header;
184 let current = &window[1].header;
185 match &previous.chain {
186 ChainBinding::NextDomain(next_domain) => {
187 if !next_domain.eq_ignore_ascii_case(¤t.d) {
188 return Dkim2Result::PermError(Error::Dkim2(
189 Dkim2Error::NextDomainMismatch(current.i),
190 ))
191 .into();
192 }
193 }
194 ChainBinding::Envelope { rcpt_to, .. } => {
195 let current_domain = match ¤t.chain {
196 ChainBinding::Envelope { mail_from, .. } => local_and_domain(mail_from).1,
197 ChainBinding::NextDomain(_) => current.d.as_str(),
198 };
199 let custody_ok = rcpt_to.iter().any(|rcpt| {
200 let (_, rcpt_domain) = local_and_domain(rcpt);
201 relaxed_domain_match(current_domain, rcpt_domain)
202 });
203 if !custody_ok {
204 let error = match ¤t.chain {
205 ChainBinding::Envelope { .. } => {
206 Dkim2Error::MailFromMismatch(current.i)
207 }
208 ChainBinding::NextDomain(_) => Dkim2Error::CustodyBreak(current.i),
209 };
210 return Dkim2Result::PermError(Error::Dkim2(error)).into();
211 }
212 }
213 }
214 }
215
216 for sig_header in signatures {
217 let signature = &sig_header.header;
218 if signature.s.is_empty() {
219 return Dkim2Result::Fail(Error::Dkim2(Dkim2Error::NoValidAlgorithm(signature.i)))
220 .into();
221 }
222
223 let mut input = Vec::with_capacity(256);
224 for (name, value) in instances
225 .iter()
226 .filter(|h| h.header.m <= signature.m)
227 .map(|h| (h.name, h.value))
228 .chain(
229 signatures
230 .iter()
231 .filter(|h| h.header.i < signature.i)
232 .map(|h| (h.name, h.value)),
233 )
234 {
235 let mut w = CanonicalizedHeaderWriter::new(&mut input, name);
236 w.write(value);
237 w.finalize();
238 }
239 strip_and_canonicalize_signature(sig_header.value, &mut input);
240
241 for value in &signature.s {
242 let key = match self
243 .txt_lookup::<DomainKey>(
244 format!("{}._domainkey.{}.", value.selector, signature.d),
245 cache_txt,
246 )
247 .await
248 {
249 Ok(key) => key,
250 Err(Error::Dns(DnsError::Resolver(_))) => {
251 return Dkim2Result::TempError(Error::Dkim2(Dkim2Error::PublicKeyFetch(
252 signature.i,
253 )))
254 .into();
255 }
256 Err(Error::Dkim(DkimError::RevokedPublicKey)) => {
257 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::PublicKeyRevoked(
258 signature.i,
259 )))
260 .into();
261 }
262 Err(_) => {
263 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::PublicKeyMissing(
264 signature.i,
265 )))
266 .into();
267 }
268 };
269
270 if matches!(value.a, Algorithm::RsaSha256 | Algorithm::RsaSha1)
271 && key.p.public_key_bits() < 1024
272 {
273 return Dkim2Result::PermError(Error::Dkim2(Dkim2Error::PublicKeySyntax(
274 signature.i,
275 )))
276 .into();
277 }
278
279 match key.p.verify_bytes(&input, &value.b, value.a) {
280 Ok(()) => {}
281 Err(Error::Crypto(CryptoError::IncompatibleAlgorithms)) => {
282 return Dkim2Result::PermError(Error::Dkim2(
283 Dkim2Error::PublicKeyAlgorithmMismatch(signature.i),
284 ))
285 .into();
286 }
287 Err(_) => {
288 return Dkim2Result::Fail(Error::Dkim2(Dkim2Error::IncorrectSignature(
289 signature.i,
290 )))
291 .into();
292 }
293 }
294 }
295 }
296
297 let algorithm = HashAlgorithm::Sha256;
298 let mut new_body = vec![];
299 let mut new_haders = vec![];
300 let mut last_body = message.raw_body();
301 let mut last_headers = message.headers.as_slice();
302
303 for header in instances.iter().rev() {
304 let instance = &header.header;
305 let Some(recorded) = instance.hashes.iter().find(|h| h.name == Some(algorithm)) else {
306 continue;
307 };
308
309 let header_hash = algorithm.headers_hash(last_headers.iter().copied());
310
311 if header_hash.as_ref() != recorded.header_hash {
312 return Dkim2Result::Fail(Error::Dkim2(Dkim2Error::HeaderHashMismatch(instance.m)))
313 .into();
314 }
315 if !body_present {
316 break;
317 }
318 let body_hash = algorithm.body_hash(last_body);
319 if body_hash.as_ref() != recorded.body_hash {
320 return Dkim2Result::Fail(Error::Dkim2(Dkim2Error::BodyHashMismatch(instance.m)))
321 .into();
322 }
323 if instance.m > 1
324 && let Some(recipe) = &instance.recipe
325 {
326 match recipe.apply(last_headers, last_body) {
327 Ok(previous) => {
328 new_body = previous;
329 let mut iter = HeaderIterator::new(&new_body);
330 new_haders = iter.by_ref().collect();
331 last_body = iter.body();
332 last_headers = new_haders.as_slice();
333 }
334 Err(_) => {
335 return Dkim2Result::Fail(Error::Dkim2(Dkim2Error::HeaderHashMismatch(
336 instance.m,
337 )))
338 .into();
339 }
340 }
341 }
342 }
343
344 if let Some(error) = flag_violation(signatures, instances, algorithm) {
345 return Dkim2Result::Fail(Error::Dkim2(error)).into();
346 }
347
348 Dkim2Output {
349 result: Dkim2Result::Pass,
350 chain: signatures
351 .iter()
352 .map(|sig_header| ChainLink {
353 signature: &sig_header.header,
354 instance: instances
355 .iter()
356 .find(|h| h.header.m == sig_header.header.m)
357 .map(|h| &h.header),
358 result: Dkim2Result::Pass,
359 custody_ok: true,
360 })
361 .collect(),
362 }
363 }
364}
365
366fn flag_violation(
367 signatures: &[Header<'_, Signature>],
368 instances: &[Header<'_, MessageInstance>],
369 algorithm: HashAlgorithm,
370) -> Option<Dkim2Error> {
371 let mut protected_m: Option<u32> = None;
372 let mut protected_i: Option<u32> = None;
373 for header in signatures {
374 let signature = &header.header;
375 if signature.flags.contains(&Flag::DoNotModify) {
376 protected_m = Some(protected_m.map_or(signature.m, |m| m.min(signature.m)));
377 }
378 if signature.flags.contains(&Flag::DoNotExplode) {
379 protected_i = Some(protected_i.map_or(signature.i, |i| i.min(signature.i)));
380 }
381 }
382
383 if let Some(protected_m) = protected_m
384 && let Some(reference) = instances
385 .iter()
386 .find(|h| h.header.m == protected_m)
387 .and_then(|h| h.header.hashes.iter().find(|h| h.name == Some(algorithm)))
388 .map(|h| (h.header_hash.as_slice(), h.body_hash.as_slice()))
389 {
390 for header in instances {
391 let instance = &header.header;
392 if instance.m > protected_m
393 && let Some(hashes) = instance.hashes.iter().find(|h| h.name == Some(algorithm))
394 && (hashes.header_hash.as_slice(), hashes.body_hash.as_slice()) != reference
395 {
396 return Some(Dkim2Error::Modified);
397 }
398 }
399 }
400
401 if let Some(protected_i) = protected_i
402 && signatures
403 .iter()
404 .any(|h| h.header.i > protected_i && h.header.flags.contains(&Flag::Exploded))
405 {
406 return Some(Dkim2Error::Exploded);
407 }
408
409 None
410}
411
412fn local_and_domain(address: &str) -> (&str, &str) {
413 let address = address.strip_prefix('<').unwrap_or(address);
414 let address = address.strip_suffix('>').unwrap_or(address);
415 match address.rsplit_once('@') {
416 Some((local, domain)) => (local, domain),
417 None => (address, ""),
418 }
419}
420
421fn address_matches(envelope: &str, signed: &str) -> bool {
423 let (el, ed) = local_and_domain(envelope);
424 let (sl, sd) = local_and_domain(signed);
425 el == sl && ed.eq_ignore_ascii_case(sd)
426}
427
428#[inline(always)]
430fn is_reverse_path(value: &str) -> bool {
431 value.starts_with('<') && value.ends_with('>')
432}
433
434#[inline(always)]
436fn require_reverse_path() -> bool {
437 #[cfg(test)]
438 {
439 test_reverse_path::required()
440 }
441 #[cfg(not(test))]
442 {
443 true
444 }
445}
446
447pub(crate) fn relaxed_domain_match(mail_from_domain: &str, signing_domain: &str) -> bool {
448 let mut current = mail_from_domain;
449 loop {
450 if current.eq_ignore_ascii_case(signing_domain) {
451 return true;
452 }
453 match current.split_once('.') {
454 Some((_, rest)) if !rest.is_empty() => current = rest,
455 _ => return false,
456 }
457 }
458}
459
460fn strip_and_canonicalize_signature(signature: &[u8], out: &mut Vec<u8>) {
462 out.extend(b"dkim2-signature:".as_slice());
463 let mut iter = signature.iter().peekable();
464 let mut last_ch = b' ';
465 while let Some(&ch) = iter.next() {
466 if !ch.is_ascii_whitespace() {
467 if matches!(ch, b's' | b'S') && matches!(last_ch, b' ' | b';') {
468 let mut found_eq = false;
469 while let Some(next_ch) = iter.peek() {
470 match next_ch {
471 b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => {
472 iter.next();
473 }
474 b'=' => {
475 found_eq = true;
476 iter.next();
477 break;
478 }
479 _ => break,
480 }
481 }
482
483 if found_eq {
484 out.push(ch);
485 out.push(b'=');
486 'next_signature: loop {
487 let mut found_colon = false;
489 for &ch in iter.by_ref() {
490 match ch {
491 b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => {}
492 b':' => {
493 out.push(ch);
494 if !found_colon {
495 found_colon = true;
496 } else {
497 break;
498 }
499 }
500 b';' => {
501 out.push(ch);
502 break 'next_signature;
503 }
504 b',' => {
505 out.push(ch);
506 continue 'next_signature;
507 }
508 _ => {
509 out.push(ch);
510 }
511 }
512 }
513
514 for &ch in iter.by_ref() {
516 match ch {
517 b';' => {
518 out.push(ch);
519 break 'next_signature;
520 }
521 b',' => {
522 out.push(ch);
523 continue 'next_signature;
524 }
525 _ => {}
526 }
527 }
528
529 break;
530 }
531 last_ch = b' ';
532 continue;
533 }
534 }
535
536 out.push(ch);
537 last_ch = ch;
538 } else {
539 last_ch = b' ';
540 }
541 }
542
543 out.extend(b"\r\n");
544}
545
546#[cfg(test)]
547mod canonicalize_test {
548 #[test]
549 fn strip_and_canonicalize_signature() {
550 for (value, expected) in [
551 (
553 "i=1; m=1; t=5; d=ex.com; mf=YQ==; rt=Yg==; s=sel:alg:U0lH;",
554 "dkim2-signature:i=1;m=1;t=5;d=ex.com;mf=YQ==;rt=Yg==;s=sel:alg:;\r\n",
555 ),
556 ("i=1; s=sel:alg:U0lH", "dkim2-signature:i=1;s=sel:alg:\r\n"),
558 ("s=a:b:U0lH,c:d:WkZa;", "dkim2-signature:s=a:b:,c:d:;\r\n"),
560 (
562 "s=sel:alg:U0lH; f=donotmodify;",
563 "dkim2-signature:s=sel:alg:;f=donotmodify;\r\n",
564 ),
565 (
567 "i=1;\r\n m=1;\r\n\ts=sel:alg:U0\r\n lH;",
568 "dkim2-signature:i=1;m=1;s=sel:alg:;\r\n",
569 ),
570 (" i=1; s=a:b:CC; ", "dkim2-signature:i=1;s=a:b:;\r\n"),
572 ("s=a:b:CC; i=1;", "dkim2-signature:s=a:b:;i=1;\r\n"),
574 (
576 "i=1;s=a:b:CC;f=exploded;",
577 "dkim2-signature:i=1;s=a:b:;f=exploded;\r\n",
578 ),
579 ("n=foo; s=a:b:CC;", "dkim2-signature:n=foo;s=a:b:;\r\n"),
581 (
583 "d=sub.ex.com; s=ed25519:ed25519-sha256:F//Dt+leS4H;",
584 "dkim2-signature:d=sub.ex.com;s=ed25519:ed25519-sha256:;\r\n",
585 ),
586 ("", "dkim2-signature:\r\n"),
588 ("d=as; s=a:b:CC;", "dkim2-signature:d=as;s=a:b:;\r\n"),
590 ("S=sel:alg:CC;", "dkim2-signature:S=sel:alg:;\r\n"),
592 (
594 "s=badset; n=a:b:c;",
595 "dkim2-signature:s=badset;n=a:b:c;\r\n",
596 ),
597 ("s=; n=a:b:c;", "dkim2-signature:s=;n=a:b:c;\r\n"),
599 ("s=sel:alg; i=1;", "dkim2-signature:s=sel:alg;i=1;\r\n"),
601 (
603 "i=1; mf=QQ s=; s=a:b:CC;",
604 "dkim2-signature:i=1;mf=QQs=;s=a:b:;\r\n",
605 ),
606 ("mf=QQ s=; n=a:b:c;", "dkim2-signature:mf=QQs=;n=a:b:c;\r\n"),
608 (
610 "rt=QQ s=,WWW; s=a:b:CC;",
611 "dkim2-signature:rt=QQs=,WWW;s=a:b:;\r\n",
612 ),
613 ("s=se l:al g:CC;", "dkim2-signature:s=sel:alg:;\r\n"),
615 ] {
616 let mut out = Vec::new();
617 super::strip_and_canonicalize_signature(value.as_bytes(), &mut out);
618 assert_eq!(
619 String::from_utf8(out).unwrap(),
620 expected,
621 "input: {value:?}"
622 );
623 }
624 }
625}
626
627#[cfg(test)]
628pub(crate) mod test_reverse_path {
629 use std::cell::Cell;
630
631 thread_local! {
632 static REQUIRED: Cell<bool> = const { Cell::new(true) };
633 }
634
635 pub(super) fn required() -> bool {
636 REQUIRED.with(Cell::get)
637 }
638
639 pub(crate) struct LenientReversePath;
642
643 impl LenientReversePath {
644 pub(crate) fn new() -> Self {
645 REQUIRED.with(|r| r.set(false));
646 LenientReversePath
647 }
648 }
649
650 impl Drop for LenientReversePath {
651 fn drop(&mut self) {
652 REQUIRED.with(|r| r.set(true));
653 }
654 }
655}
656
657#[cfg(test)]
658mod test {
659 use super::{Envelope, MAX_CHAIN_LENGTH, flag_violation};
660 use crate::dkim2::{ChainBinding, Dkim2Signed};
661 use crate::{
662 AuthenticatedMessage, Dkim2Result, Error, MessageAuthenticator,
663 common::{
664 cache::test::DummyCaches, crypto::HashAlgorithm, headers::Header,
665 parse::TxtRecordParser, verify::DomainKey,
666 },
667 dkim2::{Dkim2Error, Flag, MessageHash, MessageInstance, Signature},
668 };
669
670 fn wrap_sigs(s: &[Signature]) -> Vec<Header<'static, Signature>> {
671 s.iter()
672 .map(|x| Header::new(b"".as_slice(), b"".as_slice(), x.clone()))
673 .collect()
674 }
675
676 fn wrap_mis(m: &[MessageInstance]) -> Vec<Header<'static, MessageInstance>> {
677 m.iter()
678 .map(|x| Header::new(b"".as_slice(), b"".as_slice(), x.clone()))
679 .collect()
680 }
681
682 #[test]
683 fn flag_violation_single_pass() {
684 let alg = HashAlgorithm::Sha256;
685 let mi = |m: u32, h: &[u8]| MessageInstance {
686 m,
687 hashes: vec![MessageHash {
688 name: Some(alg),
689 header_hash: h.to_vec(),
690 body_hash: h.to_vec(),
691 }],
692 recipe: None,
693 };
694 let sig = |i: u32, m: u32, flags: Vec<Flag>| Signature {
695 i,
696 m,
697 flags,
698 ..Default::default()
699 };
700
701 let changed = [mi(1, b"a"), mi(2, b"b")];
702 let unchanged = [mi(1, b"a")];
703
704 let donotmodify = [sig(1, 1, vec![Flag::DoNotModify]), sig(2, 2, vec![])];
705 assert_eq!(
706 flag_violation(&wrap_sigs(&donotmodify), &wrap_mis(&changed), alg),
707 Some(Dkim2Error::Modified)
708 );
709 assert_eq!(
710 flag_violation(&wrap_sigs(&donotmodify[..1]), &wrap_mis(&unchanged), alg),
711 None
712 );
713
714 let explode = [
715 sig(1, 1, vec![Flag::DoNotExplode]),
716 sig(2, 1, vec![Flag::Exploded]),
717 ];
718 assert_eq!(
719 flag_violation(&wrap_sigs(&explode), &wrap_mis(&unchanged), alg),
720 Some(Dkim2Error::Exploded)
721 );
722 let explode_before = [
723 sig(1, 1, vec![Flag::Exploded]),
724 sig(2, 1, vec![Flag::DoNotExplode]),
725 ];
726 assert_eq!(
727 flag_violation(&wrap_sigs(&explode_before), &wrap_mis(&unchanged), alg),
728 None
729 );
730 }
731 use std::{
732 path::PathBuf,
733 time::{Duration, Instant},
734 };
735
736 const NOW: u64 = 1740002100;
737
738 fn resource(parts: &[&str]) -> PathBuf {
739 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
740 path.push("resources/dkim2");
741 for part in parts {
742 path.push(part);
743 }
744 path
745 }
746
747 fn load_caches() -> DummyCaches {
748 let caches = DummyCaches::new();
749 let dns = std::fs::read(resource(&["dns.json"])).unwrap();
750 let dns: serde_json::Value = serde_json::from_slice(&dns).unwrap();
751 let valid_until = Instant::now() + Duration::new(3600, 0);
752 for (domain, selectors) in dns.as_object().unwrap() {
753 for (selector, records) in selectors.as_object().unwrap() {
754 let record = records[0][1].as_str().unwrap();
755 let name = format!("{selector}.{domain}.");
756 caches.txt_add(
757 name,
758 DomainKey::parse(record.as_bytes()).unwrap(),
759 valid_until,
760 );
761 }
762 }
763 caches
764 }
765
766 async fn verify_file<A, R>(
767 resolver: &MessageAuthenticator,
768 caches: &DummyCaches,
769 name: &str,
770 envelope: Envelope<A, R>,
771 ) -> Dkim2Result
772 where
773 A: AsRef<str>,
774 R: IntoIterator<Item: AsRef<str>>,
775 {
776 let raw = std::fs::read(resource(&["expected", name])).unwrap();
777 let message = AuthenticatedMessage::parse(&raw).unwrap();
778 let params = caches.parameters(&message);
779 resolver
780 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
781 .await
782 .result()
783 .clone()
784 }
785
786 fn top_envelope(name: &str) -> (String, Vec<String>) {
787 let raw = std::fs::read(resource(&["expected", name])).unwrap();
788 let message = AuthenticatedMessage::parse(&raw).unwrap();
789 let top = message
790 .dkim2_signatures
791 .iter()
792 .map(|h| &h.header)
793 .max_by_key(|s| s.i)
794 .unwrap();
795 match &top.chain {
796 ChainBinding::Envelope { mail_from, rcpt_to } => (mail_from.clone(), rcpt_to.clone()),
797 ChainBinding::NextDomain(_) => panic!("top signature has nd="),
798 }
799 }
800
801 #[tokio::test]
802 async fn verify_golden_vectors() {
803 let resolver = MessageAuthenticator::new_system_conf().unwrap();
804 let caches = load_caches();
805
806 verify_pass_list(
807 &resolver,
808 &caches,
809 &[
810 "simple-ed25519.eml",
811 "simple-rsa2048.eml",
812 "simple-sel2.eml",
813 "simple-sel3.eml",
814 "multiheader-ed25519.eml",
815 "trailingblank-ed25519.eml",
816 "emptybody-ed25519.eml",
817 "multirecipient-ed25519.eml",
818 "dsn-ed25519.eml",
819 "dupheaders-ed25519.eml",
820 ],
821 )
822 .await;
823
824 let _lenient = super::test_reverse_path::LenientReversePath::new();
825 verify_pass_list(
826 &resolver,
827 &caches,
828 &[
829 "simple-rsa1024.eml",
830 "multihop-header-add.eml",
831 "multihop-body-footer.eml",
832 "multihop-header-replace.eml",
833 "multihop-dup-headers.eml",
834 "multihop-3hop-dup-headers.eml",
835 ],
836 )
837 .await;
838 }
839
840 async fn verify_pass_list(
841 resolver: &MessageAuthenticator,
842 caches: &DummyCaches,
843 names: &[&str],
844 ) {
845 for &name in names {
846 let (mail_from, rcpt_to) = top_envelope(name);
847 let result =
848 verify_file(resolver, caches, name, Envelope::new(&mail_from, &rcpt_to)).await;
849 assert_eq!(result, Dkim2Result::Pass, "vector {name}");
850 }
851 }
852
853 fn prepend(signed: &Dkim2Signed, message: &[u8]) -> Vec<u8> {
854 let mut out = Vec::with_capacity(message.len() + 512);
855 signed.write(&mut out);
856 out.extend_from_slice(message);
857 out
858 }
859
860 #[tokio::test]
861 async fn sign_then_verify_multi_hop() {
862 use crate::{
863 common::crypto::Ed25519Key,
864 dkim2::{Dkim2Signer, Envelope, Hop},
865 };
866 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
867
868 let load = |domain: &str, selector: &str| {
869 let pem = std::fs::read(resource(&[
870 "keys",
871 &format!("{selector}._domainkey.{domain}.pem"),
872 ]))
873 .unwrap();
874 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
875 panic!("expected PKCS8 key");
876 };
877 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
878 };
879
880 let original = std::fs::read(resource(&["emails", "simple.eml"])).unwrap();
881
882 let hop1 = Dkim2Signer::from_key(load("test1.dkim2.com", "ed25519"))
883 .domain("test1.dkim2.com")
884 .selector("ed25519");
885 let sign1 = hop1
886 .sign(
887 &original,
888 Hop::real("sender@test1.dkim2.com", ["list@test2.dkim2.com"]),
889 )
890 .unwrap();
891 let message1 = prepend(&sign1, &original);
892
893 let hop2 = Dkim2Signer::from_key(load("test2.dkim2.com", "ed25519"))
894 .domain("test2.dkim2.com")
895 .selector("ed25519");
896 let sign2 = hop2
897 .sign(
898 &message1,
899 Hop::real("relay@test2.dkim2.com", ["recipient@example.com"]),
900 )
901 .unwrap();
902 let message2 = prepend(&sign2, &message1);
903
904 let resolver = MessageAuthenticator::new_system_conf().unwrap();
905 let caches = load_caches();
906 let message = AuthenticatedMessage::parse(&message2).unwrap();
907 let params = caches.parameters(&message);
908 let envelope = Envelope::new("relay@test2.dkim2.com", ["recipient@example.com"]);
909 let output = resolver
910 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
911 .await;
912 assert_eq!(
913 output.result(),
914 &Dkim2Result::Pass,
915 "{:?}",
916 output.failure_reason()
917 );
918 assert_eq!(output.chain().len(), 2);
919 }
920
921 #[tokio::test]
922 async fn sign_then_verify_imaginary_hop() {
923 use crate::{
924 common::crypto::Ed25519Key,
925 dkim2::{Dkim2Signer, Envelope, Hop},
926 };
927 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
928
929 let load = |domain: &str| {
930 let pem = std::fs::read(resource(&[
931 "keys",
932 &format!("ed25519._domainkey.{domain}.pem"),
933 ]))
934 .unwrap();
935 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
936 panic!("expected PKCS8 key");
937 };
938 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
939 };
940 let signer = |domain: &'static str| {
941 Dkim2Signer::from_key(load(domain))
942 .domain(domain)
943 .selector("ed25519")
944 };
945
946 let original = std::fs::read(resource(&["emails", "simple.eml"])).unwrap();
949 let sign1 = signer("test1.dkim2.com")
950 .sign(
951 &original,
952 Hop::real("sender@test1.dkim2.com", ["list@test2.dkim2.com"]),
953 )
954 .unwrap();
955 let message1 = prepend(&sign1, &original);
956
957 let sign2 = signer("test2.dkim2.com")
958 .sign(&message1, Hop::imaginary("test3.dkim2.com"))
959 .unwrap();
960 let message2 = prepend(&sign2, &message1);
961 assert!(matches!(sign2.signature.chain, ChainBinding::NextDomain(_)));
962
963 let sign3 = signer("test3.dkim2.com")
964 .sign(
965 &message2,
966 Hop::real("relay@test3.dkim2.com", ["recipient@example.com"]),
967 )
968 .unwrap();
969 let message3 = prepend(&sign3, &message2);
970
971 let resolver = MessageAuthenticator::new_system_conf().unwrap();
972 let caches = load_caches();
973 let message = AuthenticatedMessage::parse(&message3).unwrap();
974 let params = caches.parameters(&message);
975 let envelope = Envelope::new("relay@test3.dkim2.com", ["recipient@example.com"]);
976 let output = resolver
977 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
978 .await;
979 assert_eq!(
980 output.result(),
981 &Dkim2Result::Pass,
982 "{:?}",
983 output.failure_reason()
984 );
985 assert_eq!(output.chain().len(), 3);
986 }
987
988 #[tokio::test]
989 async fn verify_rejects_imaginary_hop_outside_custody() {
990 use crate::{
991 common::crypto::Ed25519Key,
992 dkim2::{Dkim2Signer, Envelope, Hop},
993 };
994 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
995
996 let load = |domain: &str| {
997 let pem = std::fs::read(resource(&[
998 "keys",
999 &format!("ed25519._domainkey.{domain}.pem"),
1000 ]))
1001 .unwrap();
1002 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
1003 panic!("expected PKCS8 key");
1004 };
1005 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
1006 };
1007 let signer = |domain: &'static str| {
1008 Dkim2Signer::from_key(load(domain))
1009 .domain(domain)
1010 .selector("ed25519")
1011 };
1012
1013 let original = std::fs::read(resource(&["emails", "simple.eml"])).unwrap();
1016 let sign1 = signer("test1.dkim2.com")
1017 .sign(
1018 &original,
1019 Hop::real("sender@test1.dkim2.com", ["list@test2.dkim2.com"]),
1020 )
1021 .unwrap();
1022 let message1 = prepend(&sign1, &original);
1023
1024 let sign2 = signer("test4.dkim2.com")
1025 .sign(&message1, Hop::imaginary("test3.dkim2.com"))
1026 .unwrap();
1027 let message2 = prepend(&sign2, &message1);
1028
1029 let sign3 = signer("test3.dkim2.com")
1030 .sign(
1031 &message2,
1032 Hop::real("relay@test3.dkim2.com", ["recipient@example.com"]),
1033 )
1034 .unwrap();
1035 let message3 = prepend(&sign3, &message2);
1036
1037 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1038 let caches = load_caches();
1039 let message = AuthenticatedMessage::parse(&message3).unwrap();
1040 let params = caches.parameters(&message);
1041 let envelope = Envelope::new("relay@test3.dkim2.com", ["recipient@example.com"]);
1042 let result = resolver
1043 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
1044 .await;
1045
1046 assert_eq!(
1047 result.result(),
1048 &Dkim2Result::PermError(Error::Dkim2(Dkim2Error::CustodyBreak(2))),
1049 "{:?}",
1050 result.failure_reason()
1051 );
1052 }
1053
1054 #[tokio::test]
1055 async fn sign_multi_algorithm_then_verify() {
1056 use crate::{
1057 common::crypto::{Algorithm, Ed25519Key, RsaKey, Sha256},
1058 dkim2::{Dkim2Signer, Envelope, Hop},
1059 };
1060 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
1061
1062 let load_ed = |domain: &str, selector: &str| {
1063 let pem = std::fs::read(resource(&[
1064 "keys",
1065 &format!("{selector}._domainkey.{domain}.pem"),
1066 ]))
1067 .unwrap();
1068 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
1069 panic!("expected PKCS8 key");
1070 };
1071 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
1072 };
1073 let load_rsa = |domain: &str, selector: &str| {
1074 let pem = std::fs::read(resource(&[
1075 "keys",
1076 &format!("{selector}._domainkey.{domain}.pem"),
1077 ]))
1078 .unwrap();
1079 RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(&pem).unwrap()).unwrap()
1080 };
1081
1082 let original = std::fs::read(resource(&["emails", "simple.eml"])).unwrap();
1083
1084 let signed = Dkim2Signer::from_key(load_ed("test1.dkim2.com", "ed25519"))
1085 .domain("test1.dkim2.com")
1086 .selector("ed25519")
1087 .additional_key(load_rsa("test1.dkim2.com", "sel1"), "sel1")
1088 .sign(
1089 &original,
1090 Hop::real("sender@test1.dkim2.com", ["recipient@example.com"]),
1091 )
1092 .unwrap();
1093
1094 assert_eq!(signed.signature.s.len(), 2);
1095 assert_eq!(signed.signature.s[0].selector, "ed25519");
1096 assert_eq!(signed.signature.s[0].a, Algorithm::Ed25519Sha256);
1097 assert_eq!(signed.signature.s[1].selector, "sel1");
1098 assert_eq!(signed.signature.s[1].a, Algorithm::RsaSha256);
1099
1100 let message = prepend(&signed, &original);
1101 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1102 let caches = load_caches();
1103 let parsed = AuthenticatedMessage::parse(&message).unwrap();
1104 let params = caches.parameters(&parsed);
1105 let envelope = Envelope::new("sender@test1.dkim2.com", ["recipient@example.com"]);
1106 let output = resolver
1107 .verify_dkim2_(&parsed, envelope, params.cache_txt, NOW, true)
1108 .await;
1109 assert_eq!(
1110 output.result(),
1111 &Dkim2Result::Pass,
1112 "{:?}",
1113 output.failure_reason()
1114 );
1115 }
1116
1117 #[tokio::test]
1118 async fn verify_rejects_wrong_envelope() {
1119 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1120 let caches = load_caches();
1121 let envelope = Envelope::new("attacker@evil.example", ["recipient@example.com"]);
1122 let result = verify_file(&resolver, &caches, "simple-ed25519.eml", envelope).await;
1123 assert!(
1124 matches!(result, Dkim2Result::PermError(_)),
1125 "got {result:?}"
1126 );
1127 }
1128
1129 #[tokio::test]
1130 async fn verify_rejects_long_chains() {
1131 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1132 let caches = load_caches();
1133
1134 for (count, expect_too_long) in [
1135 (MAX_CHAIN_LENGTH, false),
1136 (MAX_CHAIN_LENGTH + 1, true),
1137 (MAX_CHAIN_LENGTH * 4, true),
1138 ] {
1139 let mut raw = Vec::new();
1140 for i in 1..=count {
1141 raw.extend_from_slice(
1142 format!(
1143 "DKIM2-Signature: i={i}; m={i}; t={NOW}; d=ex{i}.com; nd=ex{}.com; \
1144 s=sel:rsa-sha256:QQ==;\r\n",
1145 i + 1
1146 )
1147 .as_bytes(),
1148 );
1149 }
1150 raw.extend_from_slice(b"From: sender@test1.dkim2.com\r\n\r\nHello\r\n");
1151
1152 let message = AuthenticatedMessage::parse(&raw).unwrap();
1153 assert_eq!(message.dkim2_signatures.len(), count);
1154
1155 let params = caches.parameters(&message);
1156 let envelope = Envelope::new("sender@test1.dkim2.com", ["recipient@example.com"]);
1157 let result = resolver
1158 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
1159 .await;
1160
1161 assert_eq!(
1162 matches!(
1163 result.result(),
1164 Dkim2Result::PermError(Error::Dkim2(Dkim2Error::ChainTooLong))
1165 ),
1166 expect_too_long,
1167 "count={count} got {:?}",
1168 result.result()
1169 );
1170 }
1171 }
1172
1173 #[tokio::test]
1174 async fn verify_rejects_tampered_body() {
1175 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1176 let caches = load_caches();
1177 let raw = std::fs::read(resource(&["expected", "simple-ed25519.eml"])).unwrap();
1178 let mut tampered = raw.clone();
1179 let pos = tampered.windows(5).position(|w| w == b"Hello").unwrap();
1180 tampered[pos] = b'J';
1181 let message = AuthenticatedMessage::parse(&tampered).unwrap();
1182 let params = caches.parameters(&message);
1183 let envelope = Envelope::new("sender@test1.dkim2.com", ["recipient@example.com"]);
1184 let result = resolver
1185 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
1186 .await;
1187 assert!(
1188 matches!(result.result(), Dkim2Result::Fail(_)),
1189 "got {:?}",
1190 result.result()
1191 );
1192 }
1193
1194 #[tokio::test]
1195 async fn verify_rejects_tampered_header() {
1196 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1197 let caches = load_caches();
1198 let raw = std::fs::read(resource(&["expected", "simple-ed25519.eml"])).unwrap();
1199 let mut tampered = raw.clone();
1200 let pos = tampered.windows(6).position(|w| w == b"Simple").unwrap();
1201 tampered[pos] = b'X';
1202 let message = AuthenticatedMessage::parse(&tampered).unwrap();
1203 let params = caches.parameters(&message);
1204 let envelope = Envelope::new("sender@test1.dkim2.com", ["recipient@example.com"]);
1205 let result = resolver
1206 .verify_dkim2_(&message, envelope, params.cache_txt, NOW, true)
1207 .await;
1208 assert!(
1209 matches!(
1210 result.result(),
1211 Dkim2Result::Fail(Error::Dkim2(Dkim2Error::HeaderHashMismatch(_)))
1212 ),
1213 "got {:?}",
1214 result.result()
1215 );
1216 }
1217
1218 #[tokio::test]
1219 async fn verify_rejects_rcpt_not_in_rt() {
1220 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1221 let caches = load_caches();
1222 let envelope = Envelope::new("sender@test1.dkim2.com", ["someone-else@example.com"]);
1223 let result = verify_file(&resolver, &caches, "simple-ed25519.eml", envelope).await;
1224 assert!(
1225 matches!(
1226 result,
1227 Dkim2Result::PermError(Error::Dkim2(Dkim2Error::RcptToMismatch(_)))
1228 ),
1229 "got {result:?}"
1230 );
1231 }
1232
1233 fn state_matches(expected: &str, result: &Dkim2Result) -> bool {
1234 match expected {
1235 "pass" => matches!(result, Dkim2Result::Pass),
1236 "fail" => matches!(result, Dkim2Result::Fail(_)),
1237 "permerror" => matches!(result, Dkim2Result::PermError(_)),
1238 "temperror" => matches!(result, Dkim2Result::TempError(_)),
1239 other => panic!("unknown expected state {other}"),
1240 }
1241 }
1242
1243 #[tokio::test]
1244 async fn test_vectors() {
1245 let resolver = MessageAuthenticator::new_system_conf().unwrap();
1246 let caches = load_caches();
1247
1248 let cases = std::fs::read(resource(&["cases.json"])).unwrap();
1249 let cases: serde_json::Value = serde_json::from_slice(&cases).unwrap();
1250 let cases = cases.as_array().unwrap();
1251 assert!(!cases.is_empty(), "no imported vectors found");
1252
1253 let mut failures = Vec::new();
1254 for case in cases {
1255 let name = case["name"].as_str().unwrap();
1256 let expected = case["expected"].as_str().unwrap();
1257 let file = case["file"].as_str().unwrap();
1258 let mail_from = case["mail_from"].as_str().unwrap().to_string();
1259 let rcpt_to: Vec<String> = case["rcpt_to"]
1260 .as_array()
1261 .unwrap()
1262 .iter()
1263 .map(|r| r.as_str().unwrap().to_string())
1264 .collect();
1265
1266 let now = case["now"]
1267 .as_u64()
1268 .expect("vector manifest must carry now");
1269 let strict = case["strict"].as_bool().unwrap_or(true);
1270
1271 let raw = std::fs::read(resource(&["expected", file])).unwrap();
1272 let Some(message) = AuthenticatedMessage::parse(&raw) else {
1273 failures.push(format!("{name}: message failed to parse"));
1274 continue;
1275 };
1276 let params = caches.parameters(&message);
1277 let envelope = Envelope::new(&mail_from, &rcpt_to);
1278 let lenient = (!strict).then(super::test_reverse_path::LenientReversePath::new);
1279 let output = resolver
1280 .verify_dkim2_(&message, envelope, params.cache_txt, now, true)
1281 .await;
1282 drop(lenient);
1283 if !state_matches(expected, output.result()) {
1284 failures.push(format!(
1285 "{name}: expected {expected}, got {:?} ({:?})",
1286 output.result(),
1287 output.failure_reason()
1288 ));
1289 }
1290 }
1291
1292 assert!(
1293 failures.is_empty(),
1294 "{} of {} vectors diverged:\n{}",
1295 failures.len(),
1296 cases.len(),
1297 failures.join("\n")
1298 );
1299 }
1300}