1use super::{
11 ChainBinding, Dkim2Error, Dkim2Hash, Flag, HashName, MessageHash, MessageInstance, Signature,
12 SignatureValue, canonicalize::cmp_ignore_ascii_case, recipe::Recipe,
13};
14use crate::{Error, crypto::Algorithm};
15use encodify::base64;
16use memchr::memchr;
17use std::collections::HashSet;
18
19const MAX_NONCE_LEN: usize = 64;
20const MAX_LABEL_LEN: usize = 63;
21const MAX_DOMAIN_LEN: usize = 253;
22const MAX_ENTRIES_PER_ALGORITHM: usize = 2;
23const INLINE_SCAN_LIMIT: usize = 16;
24
25#[derive(Clone, Copy)]
26#[repr(u8)]
27enum SignatureTag {
28 I,
29 M,
30 T,
31 D,
32 S,
33 Mf,
34 Rt,
35 Nd,
36 N,
37 F,
38}
39
40#[derive(Clone, Copy)]
41#[repr(u8)]
42enum InstanceTag {
43 M,
44 H,
45 R,
46}
47
48impl SignatureTag {
49 fn parse(name: &[u8]) -> Option<Self> {
50 hashify::map!(name, SignatureTag,
51 b"i" => SignatureTag::I,
52 b"m" => SignatureTag::M,
53 b"t" => SignatureTag::T,
54 b"d" => SignatureTag::D,
55 b"s" => SignatureTag::S,
56 b"mf" => SignatureTag::Mf,
57 b"rt" => SignatureTag::Rt,
58 b"nd" => SignatureTag::Nd,
59 b"n" => SignatureTag::N,
60 b"f" => SignatureTag::F,
61 )
62 .copied()
63 }
64
65 fn bit(self) -> u16 {
66 1 << self as u16
67 }
68}
69
70impl InstanceTag {
71 fn parse(name: &[u8]) -> Option<Self> {
72 hashify::map!(name, InstanceTag,
73 b"m" => InstanceTag::M,
74 b"h" => InstanceTag::H,
75 b"r" => InstanceTag::R,
76 )
77 .copied()
78 }
79
80 fn bit(self) -> u16 {
81 1 << self as u16
82 }
83}
84
85#[derive(Default)]
86struct SeenTags<'x> {
87 known: u16,
88 unknown: Vec<&'x [u8]>,
89 unknown_set: HashSet<&'x [u8]>,
90}
91
92impl<'x> SeenTags<'x> {
93 fn insert_known(&mut self, bit: u16) -> bool {
94 let fresh = self.known & bit == 0;
95 self.known |= bit;
96 fresh
97 }
98
99 fn contains_known(&self, bit: u16) -> bool {
100 self.known & bit != 0
101 }
102
103 fn insert_unknown(&mut self, name: &'x [u8]) -> bool {
104 if !self.unknown_set.is_empty() {
105 return self.unknown_set.insert(name);
106 } else if self.unknown.contains(&name) {
107 return false;
108 }
109 self.unknown.push(name);
110 if self.unknown.len() > INLINE_SCAN_LIMIT {
111 self.unknown_set.extend(self.unknown.drain(..));
112 }
113 true
114 }
115}
116
117#[derive(Debug, Clone, Copy)]
118enum ParseError {
119 Syntax,
120 TagMissing(&'static str),
121 TagUnexpected(&'static str),
122 DuplicateSelector,
123 TooManySelectors,
124 DuplicateHash,
125 InvalidJson,
126}
127
128impl ParseError {
129 #[cold]
130 fn into_signature_error(self, header: &[u8]) -> Error {
131 let i = tag_number(header, b"i");
132 Error::Dkim2(match self {
133 ParseError::TagMissing(tag) => Dkim2Error::SignatureTagMissing { i, tag },
134 ParseError::TagUnexpected(tag) => Dkim2Error::SignatureTagUnexpected { i, tag },
135 ParseError::DuplicateSelector => Dkim2Error::DuplicateSelector(i),
136 ParseError::TooManySelectors => Dkim2Error::TooManySelectors(i),
137 ParseError::Syntax | ParseError::DuplicateHash | ParseError::InvalidJson => {
138 Dkim2Error::SignatureSyntax(i)
139 }
140 })
141 }
142
143 #[cold]
144 fn into_instance_error(self, header: &[u8]) -> Error {
145 let m = tag_number(header, b"m");
146 Error::Dkim2(match self {
147 ParseError::TagMissing(tag) => Dkim2Error::InstanceTagMissing { m, tag },
148 ParseError::DuplicateHash => Dkim2Error::InstanceDuplicateHash(m),
149 ParseError::InvalidJson => Dkim2Error::InvalidJson(m),
150 ParseError::Syntax
151 | ParseError::TagUnexpected(_)
152 | ParseError::DuplicateSelector
153 | ParseError::TooManySelectors => Dkim2Error::InstanceSyntax(m),
154 })
155 }
156}
157
158impl Signature {
159 pub fn parse(header: &[u8]) -> crate::Result<Signature> {
181 parse_signature(header).map_err(|err| err.into_signature_error(header))
182 }
183}
184
185fn parse_signature(header: &[u8]) -> Result<Signature, ParseError> {
186 let mut signature = Signature::default();
187 let mut seen = SeenTags::default();
188 let mut mail_from = None;
189 let mut rcpt_to = None;
190 let mut next_domain = None;
191
192 for spec in tag_list(header) {
193 let (name, value) = spec.ok_or(ParseError::Syntax)?;
194 let Some(tag) = SignatureTag::parse(name) else {
195 if is_tag_value(value) && seen.insert_unknown(name) {
196 continue;
197 }
198 return Err(ParseError::Syntax);
199 };
200 if !seen.insert_known(tag.bit()) {
201 return Err(ParseError::Syntax);
202 }
203 match tag {
204 SignatureTag::I => signature.i = parse_number(value).ok_or(ParseError::Syntax)?,
205 SignatureTag::M => signature.m = parse_number(value).ok_or(ParseError::Syntax)?,
206 SignatureTag::T => signature.t = parse_number(value).ok_or(ParseError::Syntax)?,
207 SignatureTag::D => signature.d = parse_domain(value).ok_or(ParseError::Syntax)?,
208 SignatureTag::S => signature.s = parse_signature_values(value)?,
209 SignatureTag::Mf => mail_from = Some(decode_string(value)?),
210 SignatureTag::Rt => {
211 rcpt_to = Some(
212 split_bytes(value, b',')
213 .map(decode_string)
214 .collect::<Result<Vec<_>, _>>()?,
215 );
216 }
217 SignatureTag::Nd => next_domain = Some(parse_domain(value).ok_or(ParseError::Syntax)?),
218 SignatureTag::N => signature.n = Some(parse_nonce(value).ok_or(ParseError::Syntax)?),
219 SignatureTag::F => signature.flags = parse_flags(value)?,
220 }
221 }
222
223 for (tag, name) in [
224 (SignatureTag::I, "i"),
225 (SignatureTag::M, "m"),
226 (SignatureTag::T, "t"),
227 (SignatureTag::D, "d"),
228 (SignatureTag::S, "s"),
229 ] {
230 if !seen.contains_known(tag.bit()) {
231 return Err(ParseError::TagMissing(name));
232 }
233 }
234
235 signature.chain = match (next_domain, mail_from, rcpt_to) {
236 (Some(domain), None, None) => ChainBinding::NextDomain(domain),
237 (Some(_), _, _) => return Err(ParseError::TagUnexpected("nd")),
238 (None, Some(mail_from), Some(rcpt_to)) => ChainBinding::Envelope { mail_from, rcpt_to },
239 (None, None, _) => return Err(ParseError::TagMissing("mf")),
240 (None, Some(_), None) => return Err(ParseError::TagMissing("rt")),
241 };
242
243 Ok(signature)
244}
245
246impl MessageInstance {
247 pub fn parse(header: &[u8]) -> crate::Result<MessageInstance> {
263 parse_instance(header).map_err(|err| err.into_instance_error(header))
264 }
265}
266
267fn parse_instance(header: &[u8]) -> Result<MessageInstance, ParseError> {
268 let mut instance = MessageInstance::default();
269 let mut seen = SeenTags::default();
270
271 for spec in tag_list(header) {
272 let (name, value) = spec.ok_or(ParseError::Syntax)?;
273 let Some(tag) = InstanceTag::parse(name) else {
274 if is_tag_value(value) && seen.insert_unknown(name) {
275 continue;
276 }
277 return Err(ParseError::Syntax);
278 };
279 if !seen.insert_known(tag.bit()) {
280 return Err(ParseError::Syntax);
281 }
282 match tag {
283 InstanceTag::M => instance.m = parse_number(value).ok_or(ParseError::Syntax)?,
284 InstanceTag::H => instance.hashes = parse_hashes(value)?,
285 InstanceTag::R => {
286 let json = base64::LENIENT
287 .decode(value)
288 .map_err(|_| ParseError::Syntax)?;
289 instance.recipe =
290 Some(Recipe::from_json(&json).map_err(|_| ParseError::InvalidJson)?);
291 }
292 }
293 }
294
295 for (tag, name) in [(InstanceTag::M, "m"), (InstanceTag::H, "h")] {
296 if !seen.contains_known(tag.bit()) {
297 return Err(ParseError::TagMissing(name));
298 }
299 }
300
301 Ok(instance)
302}
303
304fn tag_list(header: &[u8]) -> impl Iterator<Item = Option<(&[u8], &[u8])>> {
305 split_bytes(header, b';')
306 .map(<[u8]>::trim_ascii)
307 .filter(|spec| !spec.is_empty())
308 .map(tag_spec)
309}
310
311fn split_bytes(value: &[u8], separator: u8) -> SplitBytes<'_> {
312 SplitBytes {
313 rest: Some(value),
314 separator,
315 }
316}
317
318struct SplitBytes<'x> {
319 rest: Option<&'x [u8]>,
320 separator: u8,
321}
322
323impl<'x> Iterator for SplitBytes<'x> {
324 type Item = &'x [u8];
325
326 fn next(&mut self) -> Option<Self::Item> {
327 let (part, rest) = split_at_byte(self.rest?, self.separator);
328 self.rest = rest;
329 Some(part)
330 }
331}
332
333fn split_at_byte(value: &[u8], separator: u8) -> (&[u8], Option<&[u8]>) {
334 match memchr(separator, value) {
335 Some(pos) => (
336 value.get(..pos).unwrap_or_default(),
337 Some(value.get(pos + 1..).unwrap_or_default()),
338 ),
339 None => (value, None),
340 }
341}
342
343fn tag_spec(spec: &[u8]) -> Option<(&[u8], &[u8])> {
344 let (name, value) = split_at_byte(spec, b'=');
345 let (name, value) = (name.trim_ascii(), value?.trim_ascii());
346 let (first, rest) = name.split_first()?;
347 (first.is_ascii_alphabetic()
348 && rest
349 .iter()
350 .all(|&ch| ch.is_ascii_alphanumeric() || ch == b'_'))
351 .then_some((name, value))
352}
353
354fn is_tag_value(value: &[u8]) -> bool {
355 value
356 .iter()
357 .all(|&ch| ch.is_ascii_graphic() || matches!(ch, b' ' | b'\t' | b'\r' | b'\n'))
358}
359
360#[cold]
361fn tag_number(header: &[u8], tag: &[u8]) -> u32 {
362 tag_list(header)
363 .flatten()
364 .find(|(name, _)| *name == tag)
365 .and_then(|(_, value)| parse_number(value))
366 .unwrap_or(0)
367}
368
369fn parse_number<T: TryFrom<u64>>(value: &[u8]) -> Option<T> {
370 if value.is_empty() {
371 return None;
372 }
373 value
374 .iter()
375 .try_fold(0u64, |acc, &ch| {
376 ch.is_ascii_digit()
377 .then(|| acc.checked_mul(10)?.checked_add(u64::from(ch - b'0')))
378 .flatten()
379 })
380 .and_then(|number| T::try_from(number).ok())
381}
382
383fn parse_domain(value: &[u8]) -> Option<String> {
384 is_dns_name(value).then(|| String::from_utf8_lossy(value).to_ascii_lowercase())
385}
386
387fn is_dns_name(value: &[u8]) -> bool {
388 !value.is_empty()
389 && value.len() <= MAX_DOMAIN_LEN
390 && split_bytes(value, b'.').all(|label| {
391 !label.is_empty()
392 && label.len() <= MAX_LABEL_LEN
393 && label
394 .iter()
395 .all(|&ch| ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'_'))
396 })
397}
398
399pub(crate) fn is_domain(value: &[u8]) -> bool {
400 !value.is_empty()
401 && value.len() <= MAX_DOMAIN_LEN
402 && split_bytes(value, b'.').all(|label| label.len() <= MAX_LABEL_LEN)
403 && split_bytes(value, b'.').all(|label| match label {
404 [first, middle @ .., last] => {
405 first.is_ascii_alphanumeric()
406 && last.is_ascii_alphanumeric()
407 && middle
408 .iter()
409 .all(|&ch| ch.is_ascii_alphanumeric() || ch == b'-')
410 }
411 [single] => single.is_ascii_alphanumeric(),
412 [] => false,
413 })
414}
415
416fn parse_nonce(value: &[u8]) -> Option<String> {
417 (value.len() <= MAX_NONCE_LEN && value.iter().all(u8::is_ascii_graphic))
418 .then(|| String::from_utf8_lossy(value).into_owned())
419}
420
421pub(crate) fn is_textstring(value: &[u8]) -> bool {
422 !value.is_empty()
423 && value
424 .iter()
425 .all(|&ch| ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'_'))
426}
427
428fn decode_string(value: &[u8]) -> Result<String, ParseError> {
429 base64::LENIENT
430 .decode_to_string(value)
431 .map_err(|_| ParseError::Syntax)
432}
433
434fn parse_flags(value: &[u8]) -> Result<Vec<Flag>, ParseError> {
435 split_bytes(value, b',')
436 .map(<[u8]>::trim_ascii)
437 .filter(|flag| !flag.is_empty())
438 .map(|flag| {
439 is_textstring(flag)
440 .then(|| Flag::parse_bytes(flag))
441 .ok_or(ParseError::Syntax)
442 })
443 .collect()
444}
445
446fn parse_signature_values(value: &[u8]) -> Result<Vec<SignatureValue>, ParseError> {
447 let mut values = Vec::with_capacity(2);
448 let mut inline: [(&[u8], &[u8]); INLINE_SCAN_LIMIT] = [(&[], &[]); INLINE_SCAN_LIMIT];
449 let mut spill = Vec::new();
450 let mut count = 0;
451 for set in split_bytes(value, b',') {
452 let (selector, algorithm, signature) = split_triple(set).ok_or(ParseError::Syntax)?;
453 if !is_dns_name(selector) || !is_textstring(algorithm) {
454 return Err(ParseError::Syntax);
455 }
456 match inline.get_mut(count) {
457 Some(slot) => *slot = (selector, algorithm),
458 None => {
459 if spill.is_empty() {
460 spill.extend_from_slice(&inline);
461 }
462 spill.push((selector, algorithm));
463 }
464 }
465 count += 1;
466
467 let Some(algorithm) = Algorithm::parse_dkim2(algorithm) else {
468 continue;
469 };
470 let b = if signature.is_empty() {
471 Vec::new()
472 } else {
473 base64::LENIENT
474 .decode(signature)
475 .map_err(|_| ParseError::Syntax)?
476 };
477 values.push(SignatureValue {
478 selector: String::from_utf8_lossy(selector).into_owned(),
479 a: algorithm,
480 b,
481 });
482 }
483
484 if spill.is_empty() {
485 check_selectors_inline(inline.get(..count).unwrap_or_default())?;
486 } else {
487 check_selectors_sorted(&mut spill)?;
488 }
489 Ok(values)
490}
491
492fn check_selectors_inline(entries: &[(&[u8], &[u8])]) -> Result<(), ParseError> {
493 for (pos, (selector, algorithm)) in entries.iter().enumerate() {
494 let mut same_algorithm = 1;
495 for (other_selector, other_algorithm) in entries.iter().skip(pos + 1) {
496 if selector.eq_ignore_ascii_case(other_selector) {
497 return Err(ParseError::DuplicateSelector);
498 }
499 same_algorithm += usize::from(algorithm == other_algorithm);
500 }
501 if same_algorithm > MAX_ENTRIES_PER_ALGORITHM {
502 return Err(ParseError::TooManySelectors);
503 }
504 }
505 Ok(())
506}
507
508fn check_selectors_sorted(entries: &mut [(&[u8], &[u8])]) -> Result<(), ParseError> {
509 entries.sort_unstable_by(|(a, _), (b, _)| cmp_ignore_ascii_case(a, b));
510 if entries
511 .windows(2)
512 .any(|pair| matches!(pair, [(a, _), (b, _)] if a.eq_ignore_ascii_case(b)))
513 {
514 return Err(ParseError::DuplicateSelector);
515 }
516 entries.sort_unstable_by_key(|(_, algorithm)| *algorithm);
517 if entries.windows(MAX_ENTRIES_PER_ALGORITHM + 1).any(|run| {
518 run.iter()
519 .all(|(_, algorithm)| run.first().is_some_and(|(_, first)| first == algorithm))
520 }) {
521 return Err(ParseError::TooManySelectors);
522 }
523 Ok(())
524}
525
526fn parse_hashes(value: &[u8]) -> Result<Vec<MessageHash>, ParseError> {
527 let mut hashes = Vec::with_capacity(1);
528 for set in split_bytes(value, b',') {
529 let (name, header_hash, body_hash) = split_triple(set).ok_or(ParseError::Syntax)?;
530 if !is_textstring(name) {
531 return Err(ParseError::Syntax);
532 }
533 hashes.push(MessageHash {
534 name: Dkim2Hash::parse(name).map_or_else(
535 || HashName::Unknown(String::from_utf8_lossy(name).into()),
536 HashName::Known,
537 ),
538 header_hash: base64::LENIENT
539 .decode(header_hash)
540 .map_err(|_| ParseError::Syntax)?,
541 body_hash: base64::LENIENT
542 .decode(body_hash)
543 .map_err(|_| ParseError::Syntax)?,
544 });
545 }
546
547 let unique = if hashes.len() <= INLINE_SCAN_LIMIT {
548 hashes.iter().enumerate().all(|(pos, hash)| {
549 hashes
550 .iter()
551 .skip(pos + 1)
552 .all(|other| other.name.as_str() != hash.name.as_str())
553 })
554 } else {
555 let mut names = hashes
556 .iter()
557 .map(|hash| hash.name.as_str())
558 .collect::<Vec<_>>();
559 names.sort_unstable();
560 names.windows(2).all(|pair| pair.first() != pair.last())
561 };
562 if unique {
563 Ok(hashes)
564 } else {
565 Err(ParseError::DuplicateHash)
566 }
567}
568
569fn split_triple(set: &[u8]) -> Option<(&[u8], &[u8], &[u8])> {
570 let (first, rest) = split_at_byte(set, b':');
571 let (second, third) = split_at_byte(rest?, b':');
572 Some((first.trim_ascii(), second.trim_ascii(), third?.trim_ascii()))
573}
574
575impl Flag {
576 pub fn parse(value: &str) -> Flag {
579 Flag::parse_bytes(value.as_bytes())
580 }
581
582 fn parse_bytes(value: &[u8]) -> Flag {
583 hashify::fnc_map!(value,
584 b"feedback" => Some(Flag::Feedback),
585 b"feedhere" => Some(Flag::FeedHere),
586 b"exploded" => Some(Flag::Exploded),
587 _ => None,
588 )
589 .unwrap_or_else(|| Flag::Unknown(String::from_utf8_lossy(value).into_owned()))
590 }
591}
592
593#[cfg(test)]
594mod test {
595 use super::*;
596 use crate::AuthenticatedMessage;
597 use crate::dkim2::HashName;
598 use mail_parser::MessageParser;
599
600 #[test]
601 fn from_parsed_classifies_dkim2_headers() {
602 let raw = concat!(
603 "DKIM2-Signature: i=1; m=1; t=1740000000; d=test4.dkim2.com; mf=PHNlbmRlckB0ZXN0NC5ka2ltMi5jb20+; rt=PHJlY2lwaWVudEBleGFtcGxlLmNvbT4=; s=ed25519:ed25519-sha256:651OFNp+DdgjeVHm1EaEmnpcP6L9PWJczuJ5Oo9dzWPf0xnVgDLcVu4IMmNgW8stVVocIt7MBd8aL0Gc/lCsAA==;\r\n",
604 "Message-Instance: m=1; h=sha256:WT8nqIyG8W1R78H1QT4oZdo1SKdQrY9JHQ4fMC+IXHU=:frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;\r\n",
605 "From: sender@test4.dkim2.com\r\n",
606 "To: recipient@example.com\r\n",
607 "\r\n",
608 "body\r\n",
609 )
610 .as_bytes();
611
612 let from_parsed =
613 AuthenticatedMessage::from_parsed(&MessageParser::new().parse(raw).unwrap(), raw, true);
614 assert_eq!(from_parsed.dkim2_signatures.len(), 1);
615 assert_eq!(from_parsed.dkim2_instances.len(), 1);
616
617 assert_eq!(from_parsed, AuthenticatedMessage::parse(raw).unwrap());
618 }
619
620 #[test]
621 fn signature_parse_malformed_does_not_panic() {
622 let cases: &[&[u8]] = &[
623 b"",
624 b"garbage no equals",
625 b"i=; m=; t=; d=;",
626 b"i=1; m=1; t=0; d=example.com; mf=; s=sel",
627 b"i=1; m=1; t=0; d=example.com; mf=; s=",
628 b"i=1; m=1; t=0; d=example.com; mf=; s=,,",
629 b"i=1; m=1; t=0; d=example.com; mf=; s=sel:rsa-sha256:",
630 ];
631 for c in cases {
632 let _ = Signature::parse(c);
633 }
634 }
635
636 #[test]
637 fn signature_parse_nd_with_envelope_is_rejected() {
638 assert!(Signature::parse(b"nd=next.example; mf=a@b.com").is_err());
639 }
640
641 #[test]
642 fn header_classification_no_false_positive() {
643 let msg = b"DKIM2-Silly: foo\r\nMessage-Idle: bar\r\nFrom: a@b\r\n\r\nbody\r\n";
644 let parsed = AuthenticatedMessage::parse(msg).unwrap();
645 assert_eq!(parsed.dkim2_signatures.len(), 0);
646 assert_eq!(parsed.dkim2_instances.len(), 0);
647 }
648
649 #[test]
650 fn header_with_digit_parses() {
651 let msg = b"X-Test1-Header: v\r\nFrom: a@b\r\n\r\nbody\r\n";
652 let parsed = AuthenticatedMessage::parse(msg).unwrap();
653 assert_eq!(parsed.headers().len(), 2);
654 }
655
656 #[test]
657 fn signature_parse_reordered_tags_and_multi_sset() {
658 let v = b"f=feedback; m=2; s=sel:rsa-sha256:AAAA,sel2:ed25519-sha256:BBBB; i=3; d=ex.com; mf=; rt=Yg==; t=5; xunknown=zz";
659 let sig = Signature::parse(v).unwrap();
660 assert_eq!(sig.i, 3);
661 assert_eq!(sig.m, 2);
662 assert_eq!(sig.s.len(), 2);
663 }
664
665 #[test]
666 fn message_instance_parse_huge_recipe_does_not_panic_on_parse() {
667 let json = br#"{"b":[{"c":[1,4294967295]}]}"#;
668 let mut hdr = b"m=2; h=sha256:QQ==:Qg==; r=".to_vec();
669 base64::STANDARD.encode_append(json, &mut hdr);
670 let mi = MessageInstance::parse(&hdr).unwrap();
671 assert!(mi.recipe.is_some());
672 }
673
674 #[test]
675 fn signature_m_can_reach_u32_max() {
676 let v: &[u8] =
677 b"i=1; m=4294967295; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=feedback;";
678 let sig = Signature::parse(v).unwrap();
679 assert_eq!(sig.m, u32::MAX);
680 }
681
682 #[test]
683 fn signature_parse_decodes_null_reverse_path() {
684 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=PD4=; rt=Yg==; s=sel:rsa-sha256:QQ==;";
685 let sig = Signature::parse(v).unwrap();
686 if let ChainBinding::Envelope { mail_from, .. } = &sig.chain {
687 assert_eq!(mail_from, "<>");
688 } else {
689 panic!("expected envelope chain");
690 }
691 }
692
693 #[test]
694 fn nonce_over_64_chars_is_rejected() {
695 let long = "a".repeat(65);
696 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; n={long}; s=sel:rsa-sha256:QQ==;");
697 assert!(matches!(
698 Signature::parse(v.as_bytes()),
699 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
700 ));
701 }
702
703 #[test]
704 fn nonce_exactly_64_chars_is_accepted() {
705 let n = "a".repeat(64);
706 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; n={n}; s=sel:rsa-sha256:QQ==;");
707 let sig = Signature::parse(v.as_bytes()).unwrap();
708 assert_eq!(sig.n.as_deref(), Some(n.as_str()));
709 }
710
711 #[test]
712 fn duplicate_signature_tag_is_rejected() {
713 let v: &[u8] = b"i=1; i=2; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
714 assert!(matches!(
715 Signature::parse(v),
716 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
717 ));
718 }
719
720 #[test]
722 fn upper_case_tags_are_unknown() {
723 let v: &[u8] = b"i=1; m=1; M=2; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;";
724 assert_eq!(Signature::parse(v).unwrap().m, 1);
725 assert!(matches!(
726 Signature::parse(b"I=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;"),
727 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
728 tag: "i",
729 ..
730 }))
731 ));
732 }
733
734 #[test]
736 fn unknown_tag_duplicates() {
737 let base = "i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;";
738 assert!(
739 Signature::parse(format!("{base} longunknowntag1=a; longunknowntag2=b").as_bytes())
740 .is_ok()
741 );
742 assert!(matches!(
743 Signature::parse(format!("{base} x_y=a; x_y=b").as_bytes()),
744 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(1)))
745 ));
746 let many = (0..40).map(|n| format!("t{n}=v; ")).collect::<String>();
747 assert!(Signature::parse(format!("{base} {many}").as_bytes()).is_ok());
748 assert!(Signature::parse(format!("{base} {many} t7=v").as_bytes()).is_err());
749 assert!(Signature::parse(format!("{base} 1x=v").as_bytes()).is_err());
750 assert!(Signature::parse(format!("{base} novalue").as_bytes()).is_err());
751 }
752
753 #[test]
754 fn selector_rules() {
755 let sig = |s: &str| {
756 Signature::parse(
757 format!("i=7; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s={s};").as_bytes(),
758 )
759 };
760 assert!(matches!(
761 sig("a:rsa-sha256:QQ==,A:ed25519-sha256:QQ=="),
762 Err(Error::Dkim2(Dkim2Error::DuplicateSelector(7)))
763 ));
764 assert!(matches!(
765 sig("a:rsa-sha256:QQ==,b:rsa-sha256:QQ==,c:rsa-sha256:QQ=="),
766 Err(Error::Dkim2(Dkim2Error::TooManySelectors(7)))
767 ));
768 assert!(matches!(
769 sig("a:x-new:QQ==,b:x-new:QQ==,c:x-new:QQ==,d:ed25519-sha256:QQ=="),
770 Err(Error::Dkim2(Dkim2Error::TooManySelectors(7)))
771 ));
772 let many = (0..20)
773 .map(|n| format!("s{n}:alg{n}:QQ=="))
774 .collect::<Vec<_>>()
775 .join(",");
776 assert!(sig(&many).is_ok());
777 assert!(matches!(
778 sig(&format!("{many},S3:x:QQ==")),
779 Err(Error::Dkim2(Dkim2Error::DuplicateSelector(7)))
780 ));
781 assert!(matches!(
782 sig(&format!("{many},z:alg3:QQ==,y:alg3:QQ==")),
783 Err(Error::Dkim2(Dkim2Error::TooManySelectors(7)))
784 ));
785
786 let parsed = sig("a:mldsa44-ed25519-sha512:QQ==,b:RSA-SHA256:QQ==").unwrap();
787 assert_eq!(parsed.s.len(), 1);
788 assert_eq!(parsed.s[0].a, Algorithm::MlDsa44Ed25519Sha512);
789 }
790
791 #[test]
792 fn message_instance_rules() {
793 assert!(matches!(
794 MessageInstance::parse(b"h=sha256:QQ==:QQ==;"),
795 Err(Error::Dkim2(Dkim2Error::InstanceTagMissing {
796 m: 0,
797 tag: "m"
798 }))
799 ));
800 assert!(matches!(
801 MessageInstance::parse(b"m=3;"),
802 Err(Error::Dkim2(Dkim2Error::InstanceTagMissing {
803 m: 3,
804 tag: "h"
805 }))
806 ));
807 assert!(matches!(
808 MessageInstance::parse(b"m=3; h=sha256:QQ==:QQ==,sha256:Qg==:Qg==;"),
809 Err(Error::Dkim2(Dkim2Error::InstanceDuplicateHash(3)))
810 ));
811 let many = (0..20)
812 .map(|n| format!("h{n}:QQ==:QQ=="))
813 .collect::<Vec<_>>()
814 .join(",");
815 assert!(MessageInstance::parse(format!("m=3; h={many};").as_bytes()).is_ok());
816 assert!(matches!(
817 MessageInstance::parse(format!("m=3; h={many},h7:QQ==:QQ==;").as_bytes()),
818 Err(Error::Dkim2(Dkim2Error::InstanceDuplicateHash(3)))
819 ));
820 let instance =
821 MessageInstance::parse(b"m=3; h=sha512:QQ==:QQ==, SHA256:QQ==:QQ==, sha3:Qg==:Qg==;")
822 .unwrap();
823 assert_eq!(
824 instance
825 .hashes
826 .iter()
827 .map(|hash| hash.name.clone())
828 .collect::<Vec<_>>(),
829 vec![
830 HashName::Known(Dkim2Hash::Sha512),
831 HashName::Unknown("SHA256".into()),
832 HashName::Unknown("sha3".into()),
833 ]
834 );
835 let mut written = Vec::new();
836 instance.write_value(&mut written);
837 assert_eq!(MessageInstance::parse(&written).unwrap(), instance);
838 }
839
840 #[test]
841 fn nonce_and_flag_syntax() {
842 let sig = |extra: &str| {
843 Signature::parse(
844 format!(
845 "i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; {extra}"
846 )
847 .as_bytes(),
848 )
849 };
850 assert!(sig("n=two words").is_err());
851 assert!(sig("n=caf\u{e9}").is_err());
852 assert_eq!(
853 sig("f=Exploded, feedhere").unwrap().flags,
854 vec![Flag::Unknown("Exploded".into()), Flag::FeedHere]
855 );
856 assert!(sig("f=bad flag").is_err());
857 let with_domain = |domain: &str| {
858 Signature::parse(
859 format!("i=1; m=1; t=0; d={domain}; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;")
860 .as_bytes(),
861 )
862 };
863 let long_label = format!("{}.com", "a".repeat(64));
864 let long_name = ["abcdefghi"; 26].join(".");
865 assert!(with_domain(&format!("{}.com", "a".repeat(63))).is_ok());
866 for domain in [
867 "ex..com",
868 ".",
869 "ex.com.",
870 ".ex.com",
871 "e x.com",
872 "ex/com",
873 "ex.com\\",
874 &long_label,
875 &long_name,
876 ] {
877 assert!(with_domain(domain).is_err(), "{domain}");
878 }
879 for (domain, parsed) in [
880 ("Mail-1.Ex.com", "mail-1.ex.com"),
881 ("_dmarc_ish.ex.com", "_dmarc_ish.ex.com"),
882 ("-ex.com", "-ex.com"),
883 ] {
884 assert_eq!(with_domain(domain).unwrap().d, parsed);
885 }
886 assert_eq!(
887 Signature::parse(
888 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=my_sel.2024:rsa-sha256:QQ==;"
889 )
890 .unwrap()
891 .s[0]
892 .selector,
893 "my_sel.2024"
894 );
895 assert!(
896 Signature::parse(b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=a..b:rsa-sha256:QQ==;")
897 .is_err()
898 );
899 assert!(sig("i=4294967296").is_err());
900 }
901
902 #[test]
903 fn duplicate_message_instance_tag_is_rejected() {
904 let v: &[u8] = b"m=1; m=2; h=sha256:QQ==:Qg==;";
905 assert!(matches!(
906 MessageInstance::parse(v),
907 Err(Error::Dkim2(Dkim2Error::InstanceSyntax(_)))
908 ));
909 }
910
911 #[test]
912 fn missing_t_tag_is_rejected() {
913 let v: &[u8] = b"i=1; m=1; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
914 assert!(matches!(
915 Signature::parse(v),
916 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
917 tag: "t",
918 ..
919 }))
920 ));
921 }
922
923 #[test]
924 fn missing_s_tag_is_rejected() {
925 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==;";
926 assert!(matches!(
927 Signature::parse(v),
928 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
929 tag: "s",
930 ..
931 }))
932 ));
933 }
934
935 #[test]
936 fn nd_with_envelope_is_rejected_as_unexpected() {
937 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; nd=next.example; mf=YQ==; s=sel:rsa-sha256:QQ==;";
938 assert!(matches!(
939 Signature::parse(v),
940 Err(Error::Dkim2(Dkim2Error::SignatureTagUnexpected {
941 tag: "nd",
942 ..
943 }))
944 ));
945 }
946
947 #[test]
948 fn unknown_algorithm_in_s_is_ignored() {
949 let v: &[u8] =
950 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:,sel:ed25519-sha256:QQ==;";
951 let sig = Signature::parse(v).unwrap();
952 assert_eq!(sig.s.len(), 1);
953 assert_eq!(sig.s[0].selector, "sel");
954 }
955
956 #[test]
957 fn only_unknown_algorithms_yields_empty_s() {
958 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:;";
959 let sig = Signature::parse(v).unwrap();
960 assert!(sig.s.is_empty());
961 }
962
963 #[test]
964 fn mf_without_rt_is_rejected() {
965 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
966 assert!(matches!(
967 Signature::parse(v),
968 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
969 tag: "rt",
970 ..
971 }))
972 ));
973 }
974
975 #[test]
976 fn rt_without_mf_is_rejected() {
977 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; rt=Yg==; s=sel:rsa-sha256:QQ==;";
978 assert!(matches!(
979 Signature::parse(v),
980 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
981 tag: "mf",
982 ..
983 }))
984 ));
985 }
986
987 #[test]
988 fn missing_d_tag_is_rejected() {
989 let v: &[u8] = b"i=1; m=1; t=0; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;";
990 assert!(matches!(
991 Signature::parse(v),
992 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
993 tag: "d",
994 ..
995 }))
996 ));
997 }
998
999 #[test]
1000 fn neither_nd_nor_envelope_is_rejected() {
1001 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; s=sel:rsa-sha256:QQ==;";
1002 assert!(matches!(
1003 Signature::parse(v),
1004 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
1005 tag: "mf",
1006 ..
1007 }))
1008 ));
1009 }
1010
1011 #[test]
1012 fn rsa_sha1_set_is_dropped() {
1013 let v: &[u8] =
1014 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==,sel2:ed25519-sha256:QQ==;";
1015 let sig = Signature::parse(v).unwrap();
1016 assert_eq!(sig.s.len(), 1);
1017 assert_eq!(sig.s[0].selector, "sel2");
1018 }
1019
1020 #[test]
1021 fn only_rsa_sha1_yields_empty_s() {
1022 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==;";
1023 let sig = Signature::parse(v).unwrap();
1024 assert!(sig.s.is_empty());
1025 }
1026
1027 #[test]
1028 fn feedhere_flag_is_parsed() {
1029 let v: &[u8] =
1030 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=feedback,feedhere;";
1031 let sig = Signature::parse(v).unwrap();
1032 assert_eq!(sig.flags, vec![Flag::Feedback, Flag::FeedHere]);
1033 }
1034}