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