1use super::{
11 ChainBinding, Dkim2Error, Dkim2Hash, Dkim2Output, Dkim2Signer, Flag, MessageHash,
12 MessageInstance, Signature, SignatureValue,
13 parse::{is_domain, is_textstring},
14 recipe::Recipe,
15 verify::{StateHashes, check_custody, check_mail_from_domain, from_domain, is_from_authentic},
16};
17use crate::SystemTime;
18use crate::dkim2::BodyRecipe;
19use crate::{
20 AuthenticatedMessage, Error,
21 crypto::SigningKey,
22 dkim2::canonicalize::{CanonicalizedHeaderWriter, Digests, body_digests, header_digests},
23 dns::to_a_label,
24 headers::{Header, Writable, Writer},
25 signer::Ready,
26};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Envelope<A, R>
36where
37 A: AsRef<str>,
38 R: IntoIterator<Item: AsRef<str>>,
39{
40 pub mail_from: A,
43 pub rcpt_to: R,
45}
46
47impl<A, R> Envelope<A, R>
48where
49 A: AsRef<str>,
50 R: IntoIterator<Item: AsRef<str>>,
51{
52 pub fn new(mail_from: A, rcpt_to: R) -> Self {
55 Envelope { mail_from, rcpt_to }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum Hop<A, R, I>
66where
67 A: AsRef<str>,
68 R: IntoIterator<Item: AsRef<str>>,
69 I: Into<String>,
70{
71 Real(Envelope<A, R>),
74 Exploded(Envelope<A, R>),
80 Imaginary {
83 next_domain: I,
85 },
86}
87
88impl<A, R> Hop<A, R, String>
89where
90 A: AsRef<str>,
91 R: IntoIterator<Item: AsRef<str>>,
92{
93 pub fn real(mail_from: A, rcpt_to: R) -> Self {
100 Hop::Real(Envelope::new(mail_from, rcpt_to))
101 }
102}
103
104impl<A, R, I> Hop<A, R, I>
105where
106 A: AsRef<str>,
107 R: IntoIterator<Item: AsRef<str>>,
108 I: Into<String>,
109{
110 pub fn exploded(self) -> Self {
114 match self {
115 Hop::Real(envelope) => Hop::Exploded(envelope),
116 hop => hop,
117 }
118 }
119}
120
121impl<I> Hop<&'static str, [&'static str; 0], I>
122where
123 I: Into<String>,
124{
125 pub fn imaginary(next_domain: I) -> Self {
135 Hop::Imaginary { next_domain }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct Dkim2Signed {
145 pub message_instance: Option<MessageInstance>,
150 pub signature: Signature,
152}
153
154impl Dkim2Signed {
155 pub fn write(&self, writer: &mut impl Writer) {
159 self.signature.write(writer);
160 if let Some(instance) = &self.message_instance {
161 instance.write(writer);
162 }
163 }
164
165 pub fn to_header(&self) -> String {
168 let mut buf = Vec::new();
169 self.write(&mut buf);
170 String::from_utf8(buf).unwrap_or_default()
171 }
172}
173
174impl Dkim2Signer<Ready> {
175 pub fn sign<'x, M, A, R, I>(
207 &self,
208 message: M,
209 hop: Hop<A, R, I>,
210 verified: Option<&Dkim2Output<'_>>,
211 ) -> crate::Result<Dkim2Signed>
212 where
213 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
214 A: AsRef<str>,
215 R: IntoIterator<Item: AsRef<str>>,
216 I: Into<String>,
217 {
218 self.sign_at(message, hop, verified, now())
219 }
220
221 pub fn sign_revised<'x, O, M, A, R, I>(
272 &self,
273 original: O,
274 modified: M,
275 hop: Hop<A, R, I>,
276 verified: Option<&Dkim2Output<'_>>,
277 ) -> crate::Result<Dkim2Signed>
278 where
279 O: TryInto<AuthenticatedMessage<'x>, Error = Error>,
280 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
281 A: AsRef<str>,
282 R: IntoIterator<Item: AsRef<str>>,
283 I: Into<String>,
284 {
285 self.sign_revised_at(original, modified, hop, verified, now())
286 }
287
288 pub(crate) fn sign_revised_at<'x, O, M, A, R, I>(
289 &self,
290 original: O,
291 modified: M,
292 hop: Hop<A, R, I>,
293 verified: Option<&Dkim2Output<'_>>,
294 now: u64,
295 ) -> crate::Result<Dkim2Signed>
296 where
297 O: TryInto<AuthenticatedMessage<'x>, Error = Error>,
298 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
299 A: AsRef<str>,
300 R: IntoIterator<Item: AsRef<str>>,
301 I: Into<String>,
302 {
303 let modified = modified.try_into()?;
304 let new_instance =
305 MessageInstance::from_message(&modified, Some(&original.try_into()?), &self.hashes)?;
306
307 self.sign_internal(&modified, hop, new_instance.as_ref(), verified, now)
308 .map(|signature| Dkim2Signed {
309 message_instance: new_instance,
310 signature,
311 })
312 }
313
314 pub fn sign_with_recipe<'x, M, A, R, I>(
327 &self,
328 modified: M,
329 recipe: Recipe,
330 hop: Hop<A, R, I>,
331 verified: Option<&Dkim2Output<'_>>,
332 ) -> crate::Result<Dkim2Signed>
333 where
334 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
335 A: AsRef<str>,
336 R: IntoIterator<Item: AsRef<str>>,
337 I: Into<String>,
338 {
339 self.sign_with_recipe_at(modified, recipe, hop, verified, now())
340 }
341
342 pub fn sign_with_message_instance<'x, A, R, I>(
359 &self,
360 modified: &AuthenticatedMessage<'x>,
361 instance: Option<&MessageInstance>,
362 hop: Hop<A, R, I>,
363 verified: Option<&Dkim2Output<'_>>,
364 ) -> crate::Result<Dkim2Signed>
365 where
366 A: AsRef<str>,
367 R: IntoIterator<Item: AsRef<str>>,
368 I: Into<String>,
369 {
370 if let Some(instance) = instance {
371 let expected = modified
372 .dkim2_instances
373 .last()
374 .map_or(0, |h| h.header.m)
375 .checked_add(1)
376 .ok_or(Error::Dkim2(Dkim2Error::SequenceOverflow))?;
377 if instance.m != expected {
378 return Err(Error::Dkim2(Dkim2Error::InstanceSyntax(instance.m)));
379 }
380 StateHashes::default()
381 .check(instance, &modified.headers, Some(modified.raw_body()))
382 .map_err(Error::Dkim2)?;
383 }
384 self.sign_internal(modified, hop, instance, verified, now())
385 .map(|signature| Dkim2Signed {
386 message_instance: None,
387 signature,
388 })
389 }
390
391 pub(crate) fn sign_at<'x, M, A, R, I>(
392 &self,
393 message: M,
394 hop: Hop<A, R, I>,
395 verified: Option<&Dkim2Output<'_>>,
396 now: u64,
397 ) -> crate::Result<Dkim2Signed>
398 where
399 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
400 A: AsRef<str>,
401 R: IntoIterator<Item: AsRef<str>>,
402 I: Into<String>,
403 {
404 let message = message.try_into()?;
405 let new_instance = MessageInstance::from_message(&message, None, &self.hashes)?;
406
407 self.sign_internal(&message, hop, new_instance.as_ref(), verified, now)
408 .map(|signature| Dkim2Signed {
409 message_instance: new_instance,
410 signature,
411 })
412 }
413
414 pub(crate) fn sign_with_recipe_at<'x, M, A, R, I>(
415 &self,
416 modified: M,
417 recipe: Recipe,
418 hop: Hop<A, R, I>,
419 verified: Option<&Dkim2Output<'_>>,
420 now: u64,
421 ) -> crate::Result<Dkim2Signed>
422 where
423 M: TryInto<AuthenticatedMessage<'x>, Error = Error>,
424 A: AsRef<str>,
425 R: IntoIterator<Item: AsRef<str>>,
426 I: Into<String>,
427 {
428 recipe.validate()?;
429 let message = modified.try_into()?;
430 let new_instance = MessageInstance::from_recipe(&message, Some(recipe), &self.hashes);
431
432 self.sign_internal(&message, hop, new_instance.as_ref(), verified, now)
433 .map(|signature| Dkim2Signed {
434 message_instance: new_instance,
435 signature,
436 })
437 }
438
439 fn sign_internal<'x, A, R, I>(
440 &self,
441 parsed: &AuthenticatedMessage<'x>,
442 hop: Hop<A, R, I>,
443 new_instance: Option<&MessageInstance>,
444 verified: Option<&Dkim2Output<'_>>,
445 now: u64,
446 ) -> crate::Result<Signature>
447 where
448 A: AsRef<str>,
449 R: IntoIterator<Item: AsRef<str>>,
450 I: Into<String>,
451 {
452 let instances = parsed.dkim2_instances.as_slice();
453 let signatures = parsed.dkim2_signatures.as_slice();
454
455 if (!signatures.is_empty() || !instances.is_empty() || parsed.has_dkim2_errors)
456 && !self.out_of_band
457 && !verified.is_some_and(|output| output.is_proof_for(parsed))
458 {
459 return Err(Error::Dkim2(Dkim2Error::NotValidlySigned));
460 }
461
462 let highest_m = instances.last().map(|h| h.header.m).unwrap_or(0);
463 let highest_i = signatures.last().map(|h| h.header.i).unwrap_or(0);
464
465 let new_m = new_instance.as_ref().map(|mi| mi.m).unwrap_or(highest_m);
466 let next_i = highest_i
467 .checked_add(1)
468 .ok_or(Error::Dkim2(Dkim2Error::SequenceOverflow))?;
469
470 let domain = signing_domain(&self.domain, next_i)?;
471 if self
472 .keys
473 .iter()
474 .any(|entry| !is_domain(entry.selector.as_bytes()))
475 {
476 return Err(Error::Dkim2(Dkim2Error::SignatureSyntax(next_i)));
477 }
478 if let Some(recipe) = new_instance.and_then(|instance| instance.recipe.as_ref()) {
479 recipe.validate()?;
480 }
481 let from_changed = new_instance
482 .and_then(|instance| instance.recipe.as_ref())
483 .is_some_and(|recipe| {
484 recipe
485 .headers
486 .iter()
487 .any(|header| header.name.eq_ignore_ascii_case("from"))
488 });
489 if (signatures.is_empty() || from_changed)
490 && require_authentic_from()
491 && !is_from_authentic(from_domain(&parsed.headers).as_deref(), &domain)
492 {
493 return Err(Error::Dkim2(Dkim2Error::FromNotAuthentic(next_i)));
494 }
495
496 if new_instance.is_none()
497 && let Some(highest) = instances.last()
498 && !verified.is_some_and(|output| output.covers(parsed.raw_message))
499 {
500 StateHashes::default()
501 .check(&highest.header, &parsed.headers, Some(parsed.raw_body()))
502 .map_err(Error::Dkim2)?;
503 }
504
505 let mut flags = Vec::with_capacity(self.flags.len() + 1);
506 for flag in &self.flags {
507 let flag = match flag {
508 Flag::Unknown(name) if is_textstring(name.as_bytes()) => Flag::parse(name),
509 Flag::Unknown(_) => {
510 return Err(Error::Dkim2(Dkim2Error::SignatureSyntax(next_i)));
511 }
512 known => known.clone(),
513 };
514 if !flags.contains(&flag) {
515 flags.push(flag);
516 }
517 }
518 let (hop, exploded) = match hop {
519 Hop::Exploded(envelope) => (Hop::Real(envelope), true),
520 hop => (hop, false),
521 };
522 let chain = match hop {
523 Hop::Real(envelope) | Hop::Exploded(envelope) => {
524 let rcpt_to = envelope
525 .rcpt_to
526 .into_iter()
527 .map(|rcpt| to_reverse_path(rcpt.as_ref()))
528 .collect::<Vec<_>>();
529 if rcpt_to.is_empty() {
530 return Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
531 i: next_i,
532 tag: "rt",
533 }));
534 } else if !rcpt_to.iter().all(|rcpt| is_forward_path(rcpt)) {
535 return Err(Error::Dkim2(Dkim2Error::SignatureSyntax(next_i)));
536 }
537 if next_i > 1 && (exploded || rcpt_to.len() > 1) && !flags.contains(&Flag::Exploded)
538 {
539 flags.push(Flag::Exploded);
540 }
541 ChainBinding::Envelope {
542 mail_from: to_reverse_path(envelope.mail_from.as_ref()),
543 rcpt_to,
544 }
545 }
546 Hop::Imaginary { next_domain } => {
547 ChainBinding::NextDomain(signing_domain(&next_domain.into(), next_i)?)
548 }
549 };
550
551 let mut signature = Signature {
552 i: next_i,
553 m: new_m,
554 t: now,
555 d: domain,
556 s: self
557 .keys
558 .iter()
559 .map(|entry| SignatureValue {
560 selector: entry.selector.clone(),
561 a: entry.key.algorithm(),
562 b: Vec::new(),
563 })
564 .collect(),
565 chain,
566 n: self.nonce.clone(),
567 flags,
568 };
569 self.check_signature(&signature, signatures.last().map(|h| &h.header))
570 .map_err(Error::Dkim2)?;
571
572 let mut input = Vec::with_capacity(256 * (instances.len() + signatures.len() + 2));
573 SignatureInput {
574 instances,
575 signatures,
576 new_signature: &signature,
577 new_instance,
578 }
579 .write(&mut input);
580
581 for (value, entry) in signature.s.iter_mut().zip(&self.keys) {
582 value.b = entry.key.sign(input.as_slice())?;
583 }
584
585 Ok(signature)
586 }
587
588 fn check_signature(
589 &self,
590 signature: &Signature,
591 previous: Option<&Signature>,
592 ) -> Result<(), Dkim2Error> {
593 if signature.i == 1 && signature.flags.contains(&Flag::Exploded) {
594 return Err(Dkim2Error::SignatureTagUnexpected {
595 i: signature.i,
596 tag: "f",
597 });
598 } else if signature.n.as_ref().is_some_and(|nonce| {
599 nonce.len() > MAX_NONCE_LEN
600 || !nonce.bytes().all(|ch| ch.is_ascii_graphic() && ch != b';')
601 }) {
602 return Err(Dkim2Error::SignatureSyntax(signature.i));
603 }
604
605 for (pos, value) in signature.s.iter().enumerate() {
606 let mut same_algorithm = 1;
607 for other in signature.s.iter().skip(pos + 1) {
608 if value.selector.eq_ignore_ascii_case(&other.selector) {
609 return Err(Dkim2Error::DuplicateSelector(signature.i));
610 }
611 same_algorithm += usize::from(value.a == other.a);
612 }
613 if same_algorithm > MAX_ENTRIES_PER_ALGORITHM {
614 return Err(Dkim2Error::TooManySelectors(signature.i));
615 }
616 }
617
618 check_mail_from_domain(signature)?;
619 previous.map_or(Ok(()), |previous| check_custody(previous, signature))
620 }
621}
622
623#[inline(always)]
624fn require_authentic_from() -> bool {
625 #[cfg(test)]
626 {
627 test_authentic_from::required()
628 }
629 #[cfg(not(test))]
630 {
631 true
632 }
633}
634
635#[cfg(test)]
636pub(crate) mod test_authentic_from {
637 use std::cell::Cell;
638
639 thread_local! {
640 static REQUIRED: Cell<bool> = const { Cell::new(true) };
641 }
642
643 pub(super) fn required() -> bool {
644 REQUIRED.with(Cell::get)
645 }
646
647 pub(crate) struct LenientFrom;
648
649 impl LenientFrom {
650 pub(crate) fn new() -> Self {
651 REQUIRED.with(|r| r.set(false));
652 LenientFrom
653 }
654 }
655
656 impl Drop for LenientFrom {
657 fn drop(&mut self) {
658 REQUIRED.with(|r| r.set(true));
659 }
660 }
661}
662
663fn is_forward_path(path: &str) -> bool {
664 path.strip_prefix('<')
665 .and_then(|inner| inner.strip_suffix('>'))
666 .filter(|inner| {
667 !inner.contains(|ch: char| ch.is_ascii_whitespace() || matches!(ch, '<' | '>'))
668 })
669 .and_then(|inner| inner.rsplit_once('@'))
670 .is_some_and(|(local, domain)| !local.is_empty() && !domain.is_empty())
671}
672
673fn signing_domain(domain: &str, i: u32) -> crate::Result<String> {
674 let domain = to_a_label(domain.trim());
675 if is_domain(domain.as_bytes()) {
676 Ok(domain.into_owned())
677 } else {
678 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(i)))
679 }
680}
681
682const MAX_NONCE_LEN: usize = 64;
683const MAX_ENTRIES_PER_ALGORITHM: usize = 2;
684
685impl MessageInstance {
686 pub fn from_message(
701 message: &AuthenticatedMessage<'_>,
702 original: Option<&AuthenticatedMessage<'_>>,
703 hashes: &[Dkim2Hash],
704 ) -> crate::Result<Option<Self>> {
705 let recipe = original
706 .map(|original| Recipe::diff(original, message))
707 .transpose()?
708 .filter(|r| !r.headers.is_empty() || r.body != BodyRecipe::None);
709 Ok(Self::from_recipe(message, recipe, hashes))
710 }
711
712 pub fn from_recipe(
718 message: &AuthenticatedMessage<'_>,
719 recipe: Option<Recipe>,
720 hashes: &[Dkim2Hash],
721 ) -> Option<Self> {
722 let instances = message.dkim2_instances.as_slice();
723
724 if !instances.is_empty() && recipe.is_none() {
725 return None;
726 }
727 let mut wanted = [false; Dkim2Hash::COUNT];
728 for hash in hashes {
729 if let Some(slot) = wanted.get_mut(hash.index()) {
730 *slot = true;
731 }
732 }
733 let header_hashes = header_digests(message.headers.iter().copied(), wanted);
734 let body_hashes = body_digests(message.raw_body(), wanted);
735 let digest = |digests: &Digests, hash: Dkim2Hash| {
736 digests
737 .get(hash.index())
738 .and_then(Option::as_ref)
739 .map(|digest| digest.as_ref().to_vec())
740 .unwrap_or_default()
741 };
742
743 Some(MessageInstance {
744 m: instances
745 .last()
746 .map(|h| h.header.m)
747 .unwrap_or(0)
748 .saturating_add(1),
749 hashes: hashes
750 .iter()
751 .map(|hash| MessageHash {
752 name: (*hash).into(),
753 header_hash: digest(&header_hashes, *hash),
754 body_hash: digest(&body_hashes, *hash),
755 })
756 .collect(),
757 recipe,
758 })
759 }
760}
761
762struct SignatureInput<'x> {
763 instances: &'x [Header<'x, MessageInstance>],
764 signatures: &'x [Header<'x, Signature>],
765 new_signature: &'x Signature,
766 new_instance: Option<&'x MessageInstance>,
767}
768
769impl<'x> Writable for SignatureInput<'x> {
770 fn write(self, writer: &mut impl Writer) {
771 for header in self.instances {
772 let mut w = CanonicalizedHeaderWriter::new(writer, header.name);
773 w.write(header.value);
774 w.finalize();
775 }
776
777 if let Some(instance) = self.new_instance {
778 let mut w = CanonicalizedHeaderWriter::new(writer, b"Message-Instance");
779 instance.write_value(&mut w);
780 w.finalize();
781 }
782
783 for header in self.signatures {
784 let mut w = CanonicalizedHeaderWriter::new(writer, header.name);
785 w.write(header.value);
786 w.finalize();
787 }
788
789 let mut w = CanonicalizedHeaderWriter::new(writer, b"DKIM2-Signature");
790 self.new_signature.write_value(&mut w, true);
791 w.finalize();
792 }
793}
794
795pub(crate) fn now() -> u64 {
796 SystemTime::now()
797 .duration_since(SystemTime::UNIX_EPOCH)
798 .map(|d| d.as_secs())
799 .unwrap_or(0)
800}
801
802pub(super) fn to_reverse_path(addr: &str) -> String {
805 let addr = addr.trim();
806 let path = addr
807 .strip_prefix('<')
808 .and_then(|addr| addr.strip_suffix('>'))
809 .unwrap_or(addr);
810 match path.rsplit_once('@') {
811 Some((local, domain)) if !domain.is_ascii() => {
812 format!("<{local}@{}>", to_a_label(domain))
813 }
814 _ => format!("<{path}>"),
815 }
816}
817
818#[cfg(test)]
819mod test {
820 use super::{Dkim2Signer, Hop};
821 use crate::{
822 crypto::{Algorithm, DkimKey, Ed25519Key, MlDsa44Ed25519Key, RsaKey, Sha256},
823 dkim2::{Dkim2Hash, Dkim2Signed, MessageInstance},
824 };
825 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
826 use std::{borrow::Cow, path::PathBuf};
827
828 const TIMESTAMP: u64 = 1740000000;
829
830 fn normalize_crlf(message: &[u8]) -> Cow<'_, [u8]> {
831 let mut needs_fix = false;
832 let mut iter = message.iter().peekable();
833 while let Some(&ch) = iter.next() {
834 match ch {
835 b'\r' => {
836 if iter.peek() != Some(&&b'\n') {
837 needs_fix = true;
838 break;
839 }
840 iter.next();
841 }
842 b'\n' => {
843 needs_fix = true;
844 break;
845 }
846 _ => {}
847 }
848 }
849
850 if !needs_fix {
851 return Cow::Borrowed(message);
852 }
853
854 let mut out = Vec::with_capacity(message.len() + 16);
855 for ch in message {
856 match ch {
857 b'\r' => {}
858 b'\n' => out.extend_from_slice(b"\r\n"),
859 _ => out.push(*ch),
860 }
861 }
862 Cow::Owned(out)
863 }
864
865 fn resource(parts: &[&str]) -> PathBuf {
866 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
867 path.push("resources/dkim2");
868 for part in parts {
869 path.push(part);
870 }
871 path
872 }
873
874 #[test]
875 fn sign_with_message_instance_leaves_instance_to_caller() {
876 let raw = normalize_crlf(&std::fs::read(resource(&["emails", "simple.eml"])).unwrap())
877 .into_owned();
878 let message = crate::AuthenticatedMessage::parse(&raw).unwrap();
879 let instance = MessageInstance::from_message(&message, None, &[Dkim2Hash::Sha256]).unwrap();
880 assert_eq!(instance.as_ref().map(|instance| instance.m), Some(1));
881
882 let signed = Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
883 .domain("test1.dkim2.com")
884 .selector("ed25519")
885 .sign_with_message_instance(
886 &message,
887 instance.as_ref(),
888 Hop::real("sender@test1.dkim2.com", ["recipient@example.com"]),
889 None,
890 )
891 .unwrap();
892 assert!(signed.message_instance.is_none());
893 assert_eq!((signed.signature.i, signed.signature.m), (1, 1));
894 }
895
896 #[test]
898 fn sign_with_message_instance_checks_instance() {
899 use crate::dkim2::{BodyRecipe, Dkim2Error, HeaderRecipe, Recipe, Step};
900
901 let raw = normalize_crlf(&std::fs::read(resource(&["emails", "simple.eml"])).unwrap())
902 .into_owned();
903 let message = crate::AuthenticatedMessage::parse(&raw).unwrap();
904 let signer = Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
905 .domain("test1.dkim2.com")
906 .selector("ed25519");
907 let hop = || Hop::real("sender@test1.dkim2.com", ["recipient@example.com"]);
908 let instance = MessageInstance::from_message(&message, None, &[Dkim2Hash::Sha256])
909 .unwrap()
910 .unwrap();
911 let err = |instance: &MessageInstance| match signer
912 .sign_with_message_instance(&message, Some(instance), hop(), None)
913 .unwrap_err()
914 {
915 crate::Error::Dkim2(error) => error,
916 other => panic!("unexpected {other:?}"),
917 };
918
919 assert_eq!(
920 err(&MessageInstance {
921 m: 2,
922 ..instance.clone()
923 }),
924 Dkim2Error::InstanceSyntax(2)
925 );
926 let mut wrong = instance.clone();
927 wrong.hashes[0].body_hash[0] ^= 1;
928 assert_eq!(
929 err(&wrong),
930 Dkim2Error::BodyHashMismatch {
931 m: 1,
932 hash: Dkim2Hash::Sha256
933 }
934 );
935 let padded = MessageInstance {
936 recipe: Some(Recipe {
937 headers: vec![HeaderRecipe {
938 name: "from ".to_string(),
939 steps: vec![Step::Data(vec!["x@test1.dkim2.com".to_string()])],
940 }],
941 body: BodyRecipe::None,
942 }),
943 ..instance.clone()
944 };
945 assert_eq!(err(&padded), Dkim2Error::RecipeSyntax);
946 signer
947 .sign_with_message_instance(&message, Some(&instance), hop(), None)
948 .unwrap();
949 }
950
951 fn load_ed25519(domain: &str, selector: &str) -> Ed25519Key {
952 let pem = std::fs::read(resource(&[
953 "keys",
954 &format!("{selector}._domainkey.{domain}.pem"),
955 ]))
956 .unwrap();
957 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
958 panic!("expected PKCS8 key");
959 };
960 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
961 }
962
963 fn load_rsa(domain: &str, selector: &str) -> RsaKey<Sha256> {
964 let pem = std::fs::read(resource(&[
965 "keys",
966 &format!("{selector}._domainkey.{domain}.pem"),
967 ]))
968 .unwrap();
969 RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(&pem).unwrap()).unwrap()
970 }
971
972 fn prepend(signed: &Dkim2Signed, message: &[u8]) -> Vec<u8> {
973 let mut out = Vec::with_capacity(message.len() + 512);
974 signed.write(&mut out);
975 out.extend_from_slice(message);
976 out
977 }
978
979 fn run_vector<K: Into<DkimKey>>(
980 key: K,
981 domain: &str,
982 selector: &str,
983 email: &str,
984 mail_from: &str,
985 rcpt_to: &[&str],
986 expected_file: &str,
987 ) {
988 run_signer_vector(
989 Dkim2Signer::from_key(key).domain(domain).selector(selector),
990 email,
991 mail_from,
992 rcpt_to,
993 expected_file,
994 );
995 }
996
997 fn run_signer_vector(
998 signer: Dkim2Signer<crate::signer::Ready>,
999 email: &str,
1000 mail_from: &str,
1001 rcpt_to: &[&str],
1002 expected_file: &str,
1003 ) {
1004 let input_raw = std::fs::read(resource(&["emails", email])).unwrap();
1005 let input = input_raw.as_slice().try_into().unwrap();
1006 let instance = MessageInstance::from_message(&input, None, &signer.hashes).unwrap();
1007 let signed = signer
1008 .sign_internal(
1009 &input,
1010 Hop::real(mail_from, rcpt_to),
1011 instance.as_ref(),
1012 None,
1013 TIMESTAMP,
1014 )
1015 .map(|signature| Dkim2Signed {
1016 message_instance: instance,
1017 signature,
1018 })
1019 .unwrap();
1020 let normalized = normalize_crlf(&input_raw);
1021 let produced = prepend(&signed, normalized.as_ref());
1022
1023 if std::env::var("REGEN_DKIM2").is_ok() {
1024 std::fs::write(resource(&["expected", expected_file]), &produced).unwrap();
1025 return;
1026 }
1027 if signed
1028 .signature
1029 .s
1030 .iter()
1031 .any(|value| value.a == Algorithm::MlDsa44Ed25519Sha512)
1032 {
1033 return;
1034 }
1035
1036 let mut expected = std::fs::read(resource(&["expected", expected_file])).unwrap();
1037 if expected.ends_with(b"\r") {
1038 expected.push(b'\n');
1039 }
1040 assert_eq!(
1041 String::from_utf8_lossy(&produced),
1042 String::from_utf8_lossy(&expected),
1043 "vector {expected_file}"
1044 );
1045 }
1046
1047 #[test]
1049 fn sign_golden_dkim2_07() {
1050 let recipient: &[&str] = &["recipient@example.com"];
1051 let ed25519 = || {
1052 Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
1053 .domain("test1.dkim2.com")
1054 .selector("ed25519")
1055 };
1056 run_signer_vector(
1057 ed25519().hashes([Dkim2Hash::Sha512]),
1058 "simple.eml",
1059 "sender@test1.dkim2.com",
1060 recipient,
1061 "simple-sha512.eml",
1062 );
1063 run_signer_vector(
1064 ed25519().hashes([Dkim2Hash::Sha256, Dkim2Hash::Sha512]),
1065 "simple.eml",
1066 "sender@test1.dkim2.com",
1067 recipient,
1068 "simple-sha256-sha512.eml",
1069 );
1070 let pem = std::fs::read(resource(&[
1071 "keys",
1072 "mldsa44._domainkey.test1.dkim2.com.pem",
1073 ]))
1074 .unwrap();
1075 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
1076 panic!("expected PKCS8 key");
1077 };
1078 run_signer_vector(
1079 Dkim2Signer::from_key(
1080 MlDsa44Ed25519Key::from_pkcs8_der(der.secret_pkcs8_der()).unwrap(),
1081 )
1082 .domain("test1.dkim2.com")
1083 .selector("mldsa44"),
1084 "simple.eml",
1085 "sender@test1.dkim2.com",
1086 recipient,
1087 "simple-mldsa44.eml",
1088 );
1089 }
1090
1091 #[test]
1092 fn sign_golden_single_hop() {
1093 let recipient: &[&str] = &["recipient@example.com"];
1094
1095 run_vector(
1096 load_ed25519("test1.dkim2.com", "ed25519"),
1097 "test1.dkim2.com",
1098 "ed25519",
1099 "simple.eml",
1100 "sender@test1.dkim2.com",
1101 recipient,
1102 "simple-ed25519.eml",
1103 );
1104 run_vector(
1105 load_rsa("test1.dkim2.com", "sel1"),
1106 "test1.dkim2.com",
1107 "sel1",
1108 "simple.eml",
1109 "sender@test1.dkim2.com",
1110 recipient,
1111 "simple-rsa2048.eml",
1112 );
1113 run_vector(
1114 load_rsa("test1.dkim2.com", "sel2"),
1115 "test1.dkim2.com",
1116 "sel2",
1117 "simple.eml",
1118 "sender@test1.dkim2.com",
1119 recipient,
1120 "simple-sel2.eml",
1121 );
1122 run_vector(
1123 load_rsa("test1.dkim2.com", "sel3"),
1124 "test1.dkim2.com",
1125 "sel3",
1126 "simple.eml",
1127 "sender@test1.dkim2.com",
1128 recipient,
1129 "simple-sel3.eml",
1130 );
1131 run_vector(
1132 load_ed25519("test2.dkim2.com", "ed25519"),
1133 "test2.dkim2.com",
1134 "ed25519",
1135 "multiheader.eml",
1136 "sender@test2.dkim2.com",
1137 recipient,
1138 "multiheader-ed25519.eml",
1139 );
1140 run_vector(
1141 load_ed25519("test3.dkim2.com", "ed25519"),
1142 "test3.dkim2.com",
1143 "ed25519",
1144 "trailingblank.eml",
1145 "sender@test3.dkim2.com",
1146 recipient,
1147 "trailingblank-ed25519.eml",
1148 );
1149 run_vector(
1150 load_ed25519("test4.dkim2.com", "ed25519"),
1151 "test4.dkim2.com",
1152 "ed25519",
1153 "emptybody.eml",
1154 "sender@test4.dkim2.com",
1155 recipient,
1156 "emptybody-ed25519.eml",
1157 );
1158 run_vector(
1159 load_ed25519("test5.dkim2.com", "ed25519"),
1160 "test5.dkim2.com",
1161 "ed25519",
1162 "multirecipient.eml",
1163 "sender@test5.dkim2.com",
1164 &[
1165 "alice@example.com",
1166 "bob@example.com",
1167 "charlie@example.com",
1168 ],
1169 "multirecipient-ed25519.eml",
1170 );
1171 run_vector(
1172 load_ed25519("test1.dkim2.com", "ed25519"),
1173 "test1.dkim2.com",
1174 "ed25519",
1175 "simple.eml",
1176 "<>",
1177 recipient,
1178 "dsn-ed25519.eml",
1179 );
1180 run_vector(
1181 load_ed25519("test1.dkim2.com", "ed25519"),
1182 "test1.dkim2.com",
1183 "ed25519",
1184 "dupheaders.eml",
1185 "sender@test1.dkim2.com",
1186 recipient,
1187 "dupheaders-ed25519.eml",
1188 );
1189 }
1190
1191 #[test]
1192 fn sign_sequence_overflow_returns_error_not_panic() {
1193 let pkcs8 = Ed25519Key::generate_pkcs8().unwrap();
1194 let key = Ed25519Key::from_pkcs8_der(&pkcs8).unwrap();
1195 let signer = Dkim2Signer::from_key(key)
1196 .domain("ex.com")
1197 .selector("sel")
1198 .out_of_band_arrangement();
1199
1200 let msg = concat!(
1201 "DKIM2-Signature: i=4294967295; m=4294967295; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:ed25519-sha256:QQ==;\r\n",
1202 "Message-Instance: m=4294967295; h=sha256:QQ==:Qg==;\r\n",
1203 "From: a@ex.com\r\n",
1204 "\r\n",
1205 "body\r\n",
1206 );
1207 let result = signer.sign_at(
1208 msg.as_bytes(),
1209 Hop::real("a@ex.com", ["b@x.com"]),
1210 None,
1211 1000,
1212 );
1213 assert_eq!(
1214 result.unwrap_err(),
1215 crate::Error::Dkim2(crate::dkim2::Dkim2Error::SequenceOverflow)
1216 );
1217 }
1218}