1use super::{
11 ChainBinding, Dkim2Error, Dkim2Hash, Dkim2Output, Dkim2Signer, Flag, MessageHash,
12 MessageInstance, Signature, SignatureValue,
13 parse::{MAX_SIGNATURE_VALUES, 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(|failure| Error::Dkim2(failure.into_error()))?;
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::NotFullyVerified));
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(|failure| Error::Dkim2(failure.into_error()))?;
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().into(),
562 b: Vec::new(),
563 group: entry.group,
564 })
565 .collect(),
566 chain,
567 n: self.nonce.clone(),
568 flags,
569 };
570 self.check_signature(&signature, signatures.last().map(|h| &h.header))
571 .map_err(Error::Dkim2)?;
572
573 let mut input = Vec::with_capacity(256 * (instances.len() + signatures.len() + 2));
574 SignatureInput {
575 instances,
576 signatures,
577 new_signature: &signature,
578 new_instance,
579 }
580 .write(&mut input);
581
582 for (value, entry) in signature.s.iter_mut().zip(&self.keys) {
583 value.b = entry.key.sign(input.as_slice())?;
584 }
585
586 Ok(signature)
587 }
588
589 fn check_signature(
590 &self,
591 signature: &Signature,
592 previous: Option<&Signature>,
593 ) -> Result<(), Dkim2Error> {
594 if signature.i == 1 && signature.flags.contains(&Flag::Exploded) {
595 return Err(Dkim2Error::SignatureTagUnexpected {
596 i: signature.i,
597 tag: "f",
598 });
599 } else if signature.n.as_ref().is_some_and(|nonce| {
600 nonce.len() > MAX_NONCE_LEN
601 || !nonce.bytes().all(|ch| ch.is_ascii_graphic() && ch != b';')
602 }) {
603 return Err(Dkim2Error::SignatureSyntax(signature.i));
604 }
605
606 if signature.s.len() > MAX_SIGNATURE_VALUES {
607 return Err(Dkim2Error::TooManySelectors(signature.i));
608 }
609 for (pos, value) in signature.s.iter().enumerate() {
610 if signature
611 .s
612 .iter()
613 .skip(pos + 1)
614 .any(|other| value.selector.eq_ignore_ascii_case(&other.selector))
615 {
616 return Err(Dkim2Error::DuplicateSelector(signature.i));
617 }
618 }
619
620 check_mail_from_domain(signature)?;
621 previous.map_or(Ok(()), |previous| check_custody(previous, signature))
622 }
623}
624
625#[inline(always)]
626fn require_authentic_from() -> bool {
627 #[cfg(test)]
628 {
629 test_authentic_from::required()
630 }
631 #[cfg(not(test))]
632 {
633 true
634 }
635}
636
637#[cfg(test)]
638pub(crate) mod test_authentic_from {
639 use std::cell::Cell;
640
641 thread_local! {
642 static REQUIRED: Cell<bool> = const { Cell::new(true) };
643 }
644
645 pub(super) fn required() -> bool {
646 REQUIRED.with(Cell::get)
647 }
648
649 pub(crate) struct LenientFrom;
650
651 impl LenientFrom {
652 pub(crate) fn new() -> Self {
653 REQUIRED.with(|r| r.set(false));
654 LenientFrom
655 }
656 }
657
658 impl Drop for LenientFrom {
659 fn drop(&mut self) {
660 REQUIRED.with(|r| r.set(true));
661 }
662 }
663}
664
665fn is_forward_path(path: &str) -> bool {
666 path.strip_prefix('<')
667 .and_then(|inner| inner.strip_suffix('>'))
668 .filter(|inner| {
669 !inner.contains(|ch: char| ch.is_ascii_whitespace() || matches!(ch, '<' | '>'))
670 })
671 .and_then(|inner| inner.rsplit_once('@'))
672 .is_some_and(|(local, domain)| !local.is_empty() && !domain.is_empty())
673}
674
675fn signing_domain(domain: &str, i: u32) -> crate::Result<String> {
676 let domain = to_a_label(domain.trim());
677 if is_domain(domain.as_bytes()) {
678 Ok(domain.into_owned())
679 } else {
680 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(i)))
681 }
682}
683
684const MAX_NONCE_LEN: usize = 64;
685
686impl MessageInstance {
687 pub fn from_message(
702 message: &AuthenticatedMessage<'_>,
703 original: Option<&AuthenticatedMessage<'_>>,
704 hashes: &[Dkim2Hash],
705 ) -> crate::Result<Option<Self>> {
706 let recipe = original
707 .map(|original| Recipe::diff(original, message))
708 .transpose()?
709 .filter(|r| !r.headers.is_empty() || r.body != BodyRecipe::None);
710 Ok(Self::from_recipe(message, recipe, hashes))
711 }
712
713 pub fn from_recipe(
719 message: &AuthenticatedMessage<'_>,
720 recipe: Option<Recipe>,
721 hashes: &[Dkim2Hash],
722 ) -> Option<Self> {
723 let instances = message.dkim2_instances.as_slice();
724
725 if !instances.is_empty() && recipe.is_none() {
726 return None;
727 }
728 let mut wanted = [false; Dkim2Hash::COUNT];
729 for hash in hashes {
730 if let Some(slot) = wanted.get_mut(hash.index()) {
731 *slot = true;
732 }
733 }
734 let header_hashes = header_digests(message.headers.iter().copied(), wanted);
735 let body_hashes = body_digests(message.raw_body(), wanted);
736 let digest = |digests: &Digests, hash: Dkim2Hash| {
737 digests
738 .get(hash.index())
739 .and_then(Option::as_ref)
740 .map(|digest| digest.as_ref().to_vec())
741 .unwrap_or_default()
742 };
743
744 Some(MessageInstance {
745 m: instances
746 .last()
747 .map(|h| h.header.m)
748 .unwrap_or(0)
749 .saturating_add(1),
750 hashes: hashes
751 .iter()
752 .map(|hash| MessageHash {
753 name: (*hash).into(),
754 header_hash: digest(&header_hashes, *hash),
755 body_hash: digest(&body_hashes, *hash),
756 group: 0,
757 })
758 .collect(),
759 recipe,
760 })
761 }
762}
763
764struct SignatureInput<'x> {
765 instances: &'x [Header<'x, MessageInstance>],
766 signatures: &'x [Header<'x, Signature>],
767 new_signature: &'x Signature,
768 new_instance: Option<&'x MessageInstance>,
769}
770
771impl<'x> Writable for SignatureInput<'x> {
772 fn write(self, writer: &mut impl Writer) {
773 for header in self.instances {
774 let mut w = CanonicalizedHeaderWriter::new(writer, header.name);
775 w.write(header.value);
776 w.finalize();
777 }
778
779 if let Some(instance) = self.new_instance {
780 let mut w = CanonicalizedHeaderWriter::new(writer, b"Message-Instance");
781 instance.write_value(&mut w);
782 w.finalize();
783 }
784
785 for header in self.signatures {
786 let mut w = CanonicalizedHeaderWriter::new(writer, header.name);
787 w.write(header.value);
788 w.finalize();
789 }
790
791 let mut w = CanonicalizedHeaderWriter::new(writer, b"DKIM2-Signature");
792 self.new_signature.write_value(&mut w, true);
793 w.finalize();
794 }
795}
796
797pub(crate) fn now() -> u64 {
798 SystemTime::now()
799 .duration_since(SystemTime::UNIX_EPOCH)
800 .map(|d| d.as_secs())
801 .unwrap_or(0)
802}
803
804pub(super) fn to_reverse_path(addr: &str) -> String {
807 let addr = addr.trim();
808 let path = addr
809 .strip_prefix('<')
810 .and_then(|addr| addr.strip_suffix('>'))
811 .unwrap_or(addr);
812 match path.rsplit_once('@') {
813 Some((local, domain)) if !domain.is_ascii() => {
814 format!("<{local}@{}>", to_a_label(domain))
815 }
816 _ => format!("<{path}>"),
817 }
818}
819
820#[cfg(test)]
821mod test {
822 use super::{Dkim2Signer, Hop};
823 use crate::{
824 crypto::{Algorithm, DkimKey, Ed25519Key, MlDsa44Key, RsaKey, Sha256},
825 dkim2::{Dkim2Hash, Dkim2Signed, MessageInstance},
826 };
827 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
828 use std::{borrow::Cow, path::PathBuf};
829
830 const TIMESTAMP: u64 = 1740000000;
831
832 fn normalize_crlf(message: &[u8]) -> Cow<'_, [u8]> {
833 let mut needs_fix = false;
834 let mut iter = message.iter().peekable();
835 while let Some(&ch) = iter.next() {
836 match ch {
837 b'\r' => {
838 if iter.peek() != Some(&&b'\n') {
839 needs_fix = true;
840 break;
841 }
842 iter.next();
843 }
844 b'\n' => {
845 needs_fix = true;
846 break;
847 }
848 _ => {}
849 }
850 }
851
852 if !needs_fix {
853 return Cow::Borrowed(message);
854 }
855
856 let mut out = Vec::with_capacity(message.len() + 16);
857 for ch in message {
858 match ch {
859 b'\r' => {}
860 b'\n' => out.extend_from_slice(b"\r\n"),
861 _ => out.push(*ch),
862 }
863 }
864 Cow::Owned(out)
865 }
866
867 fn resource(parts: &[&str]) -> PathBuf {
868 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
869 path.push("resources/dkim2");
870 for part in parts {
871 path.push(part);
872 }
873 path
874 }
875
876 #[test]
877 fn sign_with_message_instance_leaves_instance_to_caller() {
878 let raw = normalize_crlf(&std::fs::read(resource(&["emails", "simple.eml"])).unwrap())
879 .into_owned();
880 let message = crate::AuthenticatedMessage::parse(&raw).unwrap();
881 let instance = MessageInstance::from_message(&message, None, &[Dkim2Hash::Sha256]).unwrap();
882 assert_eq!(instance.as_ref().map(|instance| instance.m), Some(1));
883
884 let signed = Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
885 .domain("test1.dkim2.com")
886 .selector("ed25519")
887 .sign_with_message_instance(
888 &message,
889 instance.as_ref(),
890 Hop::real("sender@test1.dkim2.com", ["recipient@example.com"]),
891 None,
892 )
893 .unwrap();
894 assert!(signed.message_instance.is_none());
895 assert_eq!((signed.signature.i, signed.signature.m), (1, 1));
896 }
897
898 #[test]
900 fn sign_with_message_instance_checks_instance() {
901 use crate::dkim2::{BodyRecipe, Dkim2Error, HeaderRecipe, Recipe, Step};
902
903 let raw = normalize_crlf(&std::fs::read(resource(&["emails", "simple.eml"])).unwrap())
904 .into_owned();
905 let message = crate::AuthenticatedMessage::parse(&raw).unwrap();
906 let signer = Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
907 .domain("test1.dkim2.com")
908 .selector("ed25519");
909 let hop = || Hop::real("sender@test1.dkim2.com", ["recipient@example.com"]);
910 let instance = MessageInstance::from_message(&message, None, &[Dkim2Hash::Sha256])
911 .unwrap()
912 .unwrap();
913 let err = |instance: &MessageInstance| match signer
914 .sign_with_message_instance(&message, Some(instance), hop(), None)
915 .unwrap_err()
916 {
917 crate::Error::Dkim2(error) => error,
918 other => panic!("unexpected {other:?}"),
919 };
920
921 assert_eq!(
922 err(&MessageInstance {
923 m: 2,
924 ..instance.clone()
925 }),
926 Dkim2Error::InstanceSyntax(2)
927 );
928 let mut wrong = instance.clone();
929 wrong.hashes[0].body_hash[0] ^= 1;
930 assert_eq!(
931 err(&wrong),
932 Dkim2Error::BodyHashMismatch {
933 m: 1,
934 hash: Dkim2Hash::Sha256
935 }
936 );
937 let padded = MessageInstance {
938 recipe: Some(Recipe {
939 headers: vec![HeaderRecipe {
940 name: "from ".to_string(),
941 steps: vec![Step::Data(vec!["x@test1.dkim2.com".to_string()])],
942 }],
943 body: BodyRecipe::None,
944 }),
945 ..instance.clone()
946 };
947 assert_eq!(err(&padded), Dkim2Error::RecipeSyntax);
948 signer
949 .sign_with_message_instance(&message, Some(&instance), hop(), None)
950 .unwrap();
951 }
952
953 fn load_ed25519(domain: &str, selector: &str) -> Ed25519Key {
954 let pem = std::fs::read(resource(&[
955 "keys",
956 &format!("{selector}._domainkey.{domain}.pem"),
957 ]))
958 .unwrap();
959 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
960 panic!("expected PKCS8 key");
961 };
962 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
963 }
964
965 fn load_rsa(domain: &str, selector: &str) -> RsaKey<Sha256> {
966 let pem = std::fs::read(resource(&[
967 "keys",
968 &format!("{selector}._domainkey.{domain}.pem"),
969 ]))
970 .unwrap();
971 RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(&pem).unwrap()).unwrap()
972 }
973
974 fn prepend(signed: &Dkim2Signed, message: &[u8]) -> Vec<u8> {
975 let mut out = Vec::with_capacity(message.len() + 512);
976 signed.write(&mut out);
977 out.extend_from_slice(message);
978 out
979 }
980
981 fn run_vector<K: Into<DkimKey>>(
982 key: K,
983 domain: &str,
984 selector: &str,
985 email: &str,
986 mail_from: &str,
987 rcpt_to: &[&str],
988 expected_file: &str,
989 ) {
990 run_signer_vector(
991 Dkim2Signer::from_key(key).domain(domain).selector(selector),
992 email,
993 mail_from,
994 rcpt_to,
995 expected_file,
996 );
997 }
998
999 fn run_signer_vector(
1000 signer: Dkim2Signer<crate::signer::Ready>,
1001 email: &str,
1002 mail_from: &str,
1003 rcpt_to: &[&str],
1004 expected_file: &str,
1005 ) {
1006 let input_raw = std::fs::read(resource(&["emails", email])).unwrap();
1007 let input = input_raw.as_slice().try_into().unwrap();
1008 let instance = MessageInstance::from_message(&input, None, &signer.hashes).unwrap();
1009 let signed = signer
1010 .sign_internal(
1011 &input,
1012 Hop::real(mail_from, rcpt_to),
1013 instance.as_ref(),
1014 None,
1015 TIMESTAMP,
1016 )
1017 .map(|signature| Dkim2Signed {
1018 message_instance: instance,
1019 signature,
1020 })
1021 .unwrap();
1022 let normalized = normalize_crlf(&input_raw);
1023 let produced = prepend(&signed, normalized.as_ref());
1024
1025 if std::env::var("REGEN_DKIM2").is_ok() {
1026 std::fs::write(resource(&["expected", expected_file]), &produced).unwrap();
1027 return;
1028 }
1029 if signed
1030 .signature
1031 .s
1032 .iter()
1033 .any(|value| value.a == Algorithm::MlDsa44.into())
1034 {
1035 return;
1036 }
1037
1038 let mut expected = std::fs::read(resource(&["expected", expected_file])).unwrap();
1039 if expected.ends_with(b"\r") {
1040 expected.push(b'\n');
1041 }
1042 assert_eq!(
1043 String::from_utf8_lossy(&produced),
1044 String::from_utf8_lossy(&expected),
1045 "vector {expected_file}"
1046 );
1047 }
1048
1049 #[test]
1051 fn sign_golden_dkim2_07() {
1052 let recipient: &[&str] = &["recipient@example.com"];
1053 let ed25519 = || {
1054 Dkim2Signer::from_key(load_ed25519("test1.dkim2.com", "ed25519"))
1055 .domain("test1.dkim2.com")
1056 .selector("ed25519")
1057 };
1058 run_signer_vector(
1059 ed25519().hashes([Dkim2Hash::Sha512]),
1060 "simple.eml",
1061 "sender@test1.dkim2.com",
1062 recipient,
1063 "simple-sha512.eml",
1064 );
1065 run_signer_vector(
1066 ed25519().hashes([Dkim2Hash::Sha256, Dkim2Hash::Sha512]),
1067 "simple.eml",
1068 "sender@test1.dkim2.com",
1069 recipient,
1070 "simple-sha256-sha512.eml",
1071 );
1072 let pem = std::fs::read(resource(&[
1073 "keys",
1074 "mldsa44._domainkey.test1.dkim2.com.pem",
1075 ]))
1076 .unwrap();
1077 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
1078 panic!("expected PKCS8 key");
1079 };
1080 run_signer_vector(
1081 Dkim2Signer::from_key(MlDsa44Key::from_pkcs8_der(der.secret_pkcs8_der()).unwrap())
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}