1use super::{
8 ChainBinding, Dkim2Error, Flag, MessageHash, MessageInstance, Signature, SignatureValue,
9};
10use crate::{
11 Error,
12 common::{
13 crypto::{Algorithm, HashAlgorithm},
14 parse::TagParser,
15 },
16};
17use mail_parser::decoders::base64::base64_decode;
18
19const I: u64 = b'i' as u64;
20const M: u64 = b'm' as u64;
21const T: u64 = b't' as u64;
22const D: u64 = b'd' as u64;
23const S: u64 = b's' as u64;
24const H: u64 = b'h' as u64;
25const R: u64 = b'r' as u64;
26const N: u64 = b'n' as u64;
27const F: u64 = b'f' as u64;
28const MF: u64 = (b'm' as u64) | ((b'f' as u64) << 8);
29const RT: u64 = (b'r' as u64) | ((b't' as u64) << 8);
30const ND: u64 = (b'n' as u64) | ((b'd' as u64) << 8);
31
32const HAS_I: u32 = 1 << 0;
33const HAS_M: u32 = 1 << 1;
34const HAS_T: u32 = 1 << 2;
35const HAS_D: u32 = 1 << 3;
36const HAS_S: u32 = 1 << 4;
37const HAS_MF: u32 = 1 << 5;
38const HAS_RT: u32 = 1 << 6;
39
40const SEEN_INLINE: usize = 16;
41
42#[derive(Default)]
43struct SeenTags {
44 inline: [u64; SEEN_INLINE],
45 len: usize,
46 spill: Vec<u64>,
47}
48
49impl SeenTags {
50 #[inline]
51 fn insert(&mut self, key: u64) -> bool {
52 if self.contains(key) {
53 return false;
54 }
55 match self.inline.get_mut(self.len) {
56 Some(slot) => {
57 *slot = key;
58 self.len += 1;
59 }
60 None => self.spill.push(key),
61 }
62 true
63 }
64
65 #[inline]
66 fn contains(&self, key: u64) -> bool {
67 self.inline
68 .get(..self.len)
69 .unwrap_or_default()
70 .contains(&key)
71 || self.spill.contains(&key)
72 }
73}
74
75impl Signature {
76 #[allow(clippy::while_let_on_iterator)]
78 pub fn parse(header: &[u8]) -> crate::Result<Signature> {
79 let mut signature = Signature::default();
80 let mut mail_from: Option<String> = None;
81 let mut rcpt_to: Vec<String> = Vec::new();
82 let mut next_domain: Option<String> = None;
83 let mut has_envelope = false;
84 let mut seen = SeenTags::default();
85 let mut present = 0u32;
86 let mut header = header.iter();
87
88 while let Some(key) = header.key() {
89 if key != u64::MAX && !seen.insert(key) {
90 return Err(Error::Dkim2(Dkim2Error::SignatureSyntax(signature.i)));
91 }
92 match key {
93 I => {
94 present |= HAS_I;
95 signature.i = header.number().unwrap_or(0) as u32
96 }
97 M => {
98 present |= HAS_M;
99 signature.m = header.number().unwrap_or(0) as u32
100 }
101 T => {
102 present |= HAS_T;
103 signature.t = header.number().unwrap_or(0)
104 }
105 D => {
106 present |= HAS_D;
107 signature.d = header.text(true)
108 }
109 S => {
110 present |= HAS_S;
111 signature.s = parse_signature_values(&header.text(false))
112 .ok_or(Error::Dkim2(Dkim2Error::SignatureSyntax(signature.i)))?;
113 }
114 MF => {
115 present |= HAS_MF;
116 mail_from = Some(
117 decode_b64_string(&header.text(false))
118 .ok_or(Error::Dkim2(Dkim2Error::SignatureSyntax(signature.i)))?,
119 );
120 has_envelope = true;
121 }
122 RT => {
123 present |= HAS_RT;
124 let value = header.text(false);
125 rcpt_to = value
126 .split(',')
127 .map(|v| {
128 decode_b64_string(v)
129 .ok_or(Error::Dkim2(Dkim2Error::SignatureSyntax(signature.i)))
130 })
131 .collect::<Result<_, _>>()?;
132 has_envelope = true;
133 }
134 ND => next_domain = Some(header.text(true)),
135 N => {
136 let nonce = header.text(false);
137 if nonce.len() > 64 {
138 return Err(Error::Dkim2(Dkim2Error::SignatureSyntax(signature.i)));
139 }
140 signature.n = Some(nonce);
141 }
142 F => {
143 signature.flags = header
144 .text(false)
145 .split(',')
146 .filter(|f| !f.is_empty())
147 .map(Flag::parse)
148 .collect();
149 }
150 _ => header.ignore(),
151 }
152 }
153
154 for (flag, tag) in [
155 (HAS_I, "i"),
156 (HAS_M, "m"),
157 (HAS_T, "t"),
158 (HAS_D, "d"),
159 (HAS_S, "s"),
160 ] {
161 if present & flag == 0 {
162 return Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
163 i: signature.i,
164 tag,
165 }));
166 }
167 }
168
169 signature.chain = match (next_domain, has_envelope) {
170 (Some(_), true) => {
171 return Err(Error::Dkim2(Dkim2Error::SignatureTagUnexpected {
172 i: signature.i,
173 tag: "nd",
174 }));
175 }
176 (Some(domain), false) => ChainBinding::NextDomain(domain),
177 (None, _) => {
178 for (flag, tag) in [(HAS_MF, "mf"), (HAS_RT, "rt")] {
179 if present & flag == 0 {
180 return Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
181 i: signature.i,
182 tag,
183 }));
184 }
185 }
186 ChainBinding::Envelope {
187 mail_from: mail_from.unwrap_or_default(),
188 rcpt_to,
189 }
190 }
191 };
192
193 Ok(signature)
194 }
195}
196
197impl MessageInstance {
198 #[allow(clippy::while_let_on_iterator)]
200 pub fn parse(header: &[u8]) -> crate::Result<MessageInstance> {
201 let mut instance = MessageInstance::default();
202 let mut seen = SeenTags::default();
203 let mut header = header.iter();
204
205 while let Some(key) = header.key() {
206 if key != u64::MAX && !seen.insert(key) {
207 return Err(Error::Dkim2(Dkim2Error::InstanceSyntax(instance.m)));
208 }
209 match key {
210 M => instance.m = header.number().unwrap_or(0) as u32,
211 H => {
212 instance.hashes = parse_hashes(&header.text(false))
213 .ok_or(Error::Dkim2(Dkim2Error::InstanceSyntax(instance.m)))?;
214 }
215 R => {
216 let encoded = header.text(false);
217 let json = base64_decode(encoded.as_bytes()).ok_or(Error::Base64)?;
218 instance.recipe = Some(super::recipe::Recipe::from_json(&json)?);
219 }
220 _ => header.ignore(),
221 }
222 }
223
224 Ok(instance)
225 }
226}
227
228pub(crate) fn decode_b64_string(value: &str) -> Option<String> {
229 base64_decode(value.as_bytes()).and_then(|bytes| String::from_utf8(bytes).ok())
230}
231
232pub(crate) fn parse_signature_values(value: &str) -> Option<Vec<SignatureValue>> {
233 let value = value.trim();
234 if value.is_empty() {
235 return None;
236 }
237 let mut values = Vec::new();
238 for set in value.split(',') {
239 let mut parts = set.splitn(3, ':');
240 let selector = parts.next()?.trim();
241 let algorithm = parts.next()?.trim();
242 let signature = parts.next()?.trim();
243 let Some(algorithm) = Algorithm::parse(algorithm.as_bytes()) else {
244 continue;
245 };
246 if matches!(algorithm, Algorithm::RsaSha1) {
247 continue;
248 }
249 let b = if signature.is_empty() {
250 Vec::new()
251 } else {
252 base64_decode(signature.as_bytes())?
253 };
254 values.push(SignatureValue {
255 selector: selector.to_string(),
256 a: algorithm,
257 b,
258 });
259 }
260 Some(values)
261}
262
263pub(crate) fn parse_hashes(value: &str) -> Option<Vec<MessageHash>> {
264 let mut hashes = Vec::new();
265 for set in value.split(',') {
266 let mut parts = set.splitn(3, ':');
267 let name = parts.next()?.trim();
268 let header_hash = base64_decode(parts.next()?.trim().as_bytes())?;
269 let body_hash = base64_decode(parts.next()?.trim().as_bytes())?;
270 hashes.push(MessageHash {
271 name: HashAlgorithm::parse(name),
272 header_hash,
273 body_hash,
274 });
275 }
276 Some(hashes)
277}
278
279impl Flag {
280 pub fn parse(value: &str) -> Flag {
281 hashify::tiny_map!(value.as_bytes(),
282 b"donotmodify" => Flag::DoNotModify,
283 b"donotexplode" => Flag::DoNotExplode,
284 b"feedback" => Flag::Feedback,
285 b"feedhere" => Flag::FeedHere,
286 b"exploded" => Flag::Exploded,
287 )
288 .unwrap_or_else(|| Flag::Unknown(value.to_string()))
289 }
290}
291
292#[cfg(test)]
293mod test {
294 use super::*;
295 use crate::AuthenticatedMessage;
296 use mail_parser::MessageParser;
297
298 #[test]
299 fn from_parsed_classifies_dkim2_headers() {
300 let raw = concat!(
301 "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",
302 "Message-Instance: m=1; h=sha256:WT8nqIyG8W1R78H1QT4oZdo1SKdQrY9JHQ4fMC+IXHU=:frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;\r\n",
303 "From: sender@test4.dkim2.com\r\n",
304 "To: recipient@example.com\r\n",
305 "\r\n",
306 "body\r\n",
307 )
308 .as_bytes();
309
310 let from_parsed =
311 AuthenticatedMessage::from_parsed(&MessageParser::new().parse(raw).unwrap(), raw, true);
312 assert_eq!(from_parsed.dkim2_signatures.len(), 1);
313 assert_eq!(from_parsed.dkim2_instances.len(), 1);
314
315 assert_eq!(from_parsed, AuthenticatedMessage::parse(raw).unwrap());
316 }
317
318 #[test]
319 fn signature_parse_malformed_does_not_panic() {
320 let cases: &[&[u8]] = &[
321 b"",
322 b"garbage no equals",
323 b"i=; m=; t=; d=;",
324 b"i=1; m=1; t=0; d=example.com; mf=; s=sel",
325 b"i=1; m=1; t=0; d=example.com; mf=; s=",
326 b"i=1; m=1; t=0; d=example.com; mf=; s=,,",
327 b"i=1; m=1; t=0; d=example.com; mf=; s=sel:rsa-sha256:",
328 ];
329 for c in cases {
330 let _ = Signature::parse(c);
331 }
332 }
333
334 #[test]
335 fn signature_parse_nd_with_envelope_is_rejected() {
336 assert!(Signature::parse(b"nd=next.example; mf=a@b.com").is_err());
337 }
338
339 #[test]
340 fn header_classification_no_false_positive() {
341 let msg = b"DKIM2-Silly: foo\r\nMessage-Idle: bar\r\nFrom: a@b\r\n\r\nbody\r\n";
342 let parsed = AuthenticatedMessage::parse(msg).unwrap();
343 assert_eq!(parsed.dkim2_signatures.len(), 0);
344 assert_eq!(parsed.dkim2_instances.len(), 0);
345 }
346
347 #[test]
348 fn header_with_digit_parses() {
349 let msg = b"X-Test1-Header: v\r\nFrom: a@b\r\n\r\nbody\r\n";
350 let parsed = AuthenticatedMessage::parse(msg).unwrap();
351 assert_eq!(parsed.raw_parsed_headers().len(), 2);
352 }
353
354 #[test]
355 fn signature_parse_reordered_tags_and_multi_sset() {
356 let v = b"f=donotmodify; m=2; s=sel:rsa-sha256:AAAA,sel2:ed25519-sha256:BBBB; i=3; d=ex.com; mf=; rt=Yg==; t=5; xunknown=zz";
357 let sig = Signature::parse(v).unwrap();
358 assert_eq!(sig.i, 3);
359 assert_eq!(sig.m, 2);
360 assert_eq!(sig.s.len(), 2);
361 }
362
363 #[test]
364 fn message_instance_parse_huge_recipe_does_not_panic_on_parse() {
365 let json = br#"{"b":[{"c":[1,4294967295]}]}"#;
366 let b64 = mail_builder::encoders::Base64Encoder::new()
367 .encode(json)
368 .unwrap();
369 let mut hdr = b"m=2; h=sha256:QQ==:Qg==; r=".to_vec();
370 hdr.extend_from_slice(&b64);
371 let mi = MessageInstance::parse(&hdr).unwrap();
372 assert!(mi.recipe.is_some());
373 }
374
375 #[test]
376 fn signature_m_can_reach_u32_max() {
377 let v: &[u8] =
378 b"i=1; m=4294967295; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=donotmodify;";
379 let sig = Signature::parse(v).unwrap();
380 assert_eq!(sig.m, u32::MAX);
381 }
382
383 #[test]
384 fn signature_parse_decodes_null_reverse_path() {
385 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=PD4=; rt=Yg==; s=sel:rsa-sha256:QQ==;";
386 let sig = Signature::parse(v).unwrap();
387 if let ChainBinding::Envelope { mail_from, .. } = &sig.chain {
388 assert_eq!(mail_from, "<>");
389 } else {
390 panic!("expected envelope chain");
391 }
392 }
393
394 #[test]
395 fn nonce_over_64_chars_is_rejected() {
396 let long = "a".repeat(65);
397 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; n={long}; s=sel:rsa-sha256:QQ==;");
398 assert!(matches!(
399 Signature::parse(v.as_bytes()),
400 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
401 ));
402 }
403
404 #[test]
405 fn nonce_exactly_64_chars_is_accepted() {
406 let n = "a".repeat(64);
407 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; n={n}; s=sel:rsa-sha256:QQ==;");
408 let sig = Signature::parse(v.as_bytes()).unwrap();
409 assert_eq!(sig.n.as_deref(), Some(n.as_str()));
410 }
411
412 #[test]
413 fn duplicate_signature_tag_is_rejected() {
414 let v: &[u8] = b"i=1; i=2; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
415 assert!(matches!(
416 Signature::parse(v),
417 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
418 ));
419 }
420
421 #[test]
422 fn duplicate_signature_tag_case_insensitive_is_rejected() {
423 let v: &[u8] = b"i=1; m=1; M=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
424 assert!(Signature::parse(v).is_err());
425 }
426
427 #[test]
428 fn duplicate_message_instance_tag_is_rejected() {
429 let v: &[u8] = b"m=1; m=2; h=sha256:QQ==:Qg==;";
430 assert!(matches!(
431 MessageInstance::parse(v),
432 Err(Error::Dkim2(Dkim2Error::InstanceSyntax(_)))
433 ));
434 }
435
436 #[test]
437 fn missing_t_tag_is_rejected() {
438 let v: &[u8] = b"i=1; m=1; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
439 assert!(matches!(
440 Signature::parse(v),
441 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
442 tag: "t",
443 ..
444 }))
445 ));
446 }
447
448 #[test]
449 fn missing_s_tag_is_rejected() {
450 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==;";
451 assert!(matches!(
452 Signature::parse(v),
453 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
454 tag: "s",
455 ..
456 }))
457 ));
458 }
459
460 #[test]
461 fn nd_with_envelope_is_rejected_as_unexpected() {
462 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; nd=next.example; mf=YQ==; s=sel:rsa-sha256:QQ==;";
463 assert!(matches!(
464 Signature::parse(v),
465 Err(Error::Dkim2(Dkim2Error::SignatureTagUnexpected {
466 tag: "nd",
467 ..
468 }))
469 ));
470 }
471
472 #[test]
473 fn unknown_algorithm_in_s_is_ignored() {
474 let v: &[u8] =
475 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:,sel:ed25519-sha256:QQ==;";
476 let sig = Signature::parse(v).unwrap();
477 assert_eq!(sig.s.len(), 1);
478 assert_eq!(sig.s[0].selector, "sel");
479 }
480
481 #[test]
482 fn only_unknown_algorithms_yields_empty_s() {
483 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:;";
484 let sig = Signature::parse(v).unwrap();
485 assert!(sig.s.is_empty());
486 }
487
488 #[test]
489 fn mf_without_rt_is_rejected() {
490 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
491 assert!(matches!(
492 Signature::parse(v),
493 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
494 tag: "rt",
495 ..
496 }))
497 ));
498 }
499
500 #[test]
501 fn rt_without_mf_is_rejected() {
502 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; rt=Yg==; s=sel:rsa-sha256:QQ==;";
503 assert!(matches!(
504 Signature::parse(v),
505 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
506 tag: "mf",
507 ..
508 }))
509 ));
510 }
511
512 #[test]
513 fn missing_d_tag_is_rejected() {
514 let v: &[u8] = b"i=1; m=1; t=0; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;";
515 assert!(matches!(
516 Signature::parse(v),
517 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
518 tag: "d",
519 ..
520 }))
521 ));
522 }
523
524 #[test]
525 fn neither_nd_nor_envelope_is_rejected() {
526 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; s=sel:rsa-sha256:QQ==;";
527 assert!(matches!(
528 Signature::parse(v),
529 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
530 tag: "mf",
531 ..
532 }))
533 ));
534 }
535
536 #[test]
537 fn rsa_sha1_set_is_dropped() {
538 let v: &[u8] =
539 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==,sel2:ed25519-sha256:QQ==;";
540 let sig = Signature::parse(v).unwrap();
541 assert_eq!(sig.s.len(), 1);
542 assert_eq!(sig.s[0].selector, "sel2");
543 }
544
545 #[test]
546 fn only_rsa_sha1_yields_empty_s() {
547 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==;";
548 let sig = Signature::parse(v).unwrap();
549 assert!(sig.s.is_empty());
550 }
551
552 #[test]
553 fn feedhere_flag_is_parsed() {
554 let v: &[u8] =
555 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=feedback,feedhere;";
556 let sig = Signature::parse(v).unwrap();
557 assert_eq!(sig.flags, vec![Flag::Feedback, Flag::FeedHere]);
558 }
559}