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