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
238 #[test]
239 fn signature_parse_malformed_does_not_panic() {
240 let cases: &[&[u8]] = &[
241 b"",
242 b"garbage no equals",
243 b"i=; m=; t=; d=;",
244 b"i=1; m=1; t=0; d=example.com; mf=; s=sel",
245 b"i=1; m=1; t=0; d=example.com; mf=; s=",
246 b"i=1; m=1; t=0; d=example.com; mf=; s=,,",
247 b"i=1; m=1; t=0; d=example.com; mf=; s=sel:rsa-sha256:",
248 ];
249 for c in cases {
250 let _ = Signature::parse(c);
251 }
252 }
253
254 #[test]
255 fn signature_parse_nd_with_envelope_is_rejected() {
256 assert!(Signature::parse(b"nd=next.example; mf=a@b.com").is_err());
257 }
258
259 #[test]
260 fn header_classification_no_false_positive() {
261 let msg = b"DKIM2-Silly: foo\r\nMessage-Idle: bar\r\nFrom: a@b\r\n\r\nbody\r\n";
262 let parsed = AuthenticatedMessage::parse(msg).unwrap();
263 assert_eq!(parsed.dkim2_signatures.len(), 0);
264 assert_eq!(parsed.dkim2_instances.len(), 0);
265 }
266
267 #[test]
268 fn header_with_digit_parses() {
269 let msg = b"X-Test1-Header: v\r\nFrom: a@b\r\n\r\nbody\r\n";
270 let parsed = AuthenticatedMessage::parse(msg).unwrap();
271 assert_eq!(parsed.raw_parsed_headers().len(), 2);
272 }
273
274 #[test]
275 fn signature_parse_reordered_tags_and_multi_sset() {
276 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";
277 let sig = Signature::parse(v).unwrap();
278 assert_eq!(sig.i, 3);
279 assert_eq!(sig.m, 2);
280 assert_eq!(sig.s.len(), 2);
281 }
282
283 #[test]
284 fn message_instance_parse_huge_recipe_does_not_panic_on_parse() {
285 let json = br#"{"b":[{"c":[1,4294967295]}]}"#;
286 let b64 = mail_builder::encoders::base64::base64_encode(json).unwrap();
287 let mut hdr = b"m=2; h=sha256:QQ==:Qg==; r=".to_vec();
288 hdr.extend_from_slice(&b64);
289 let mi = MessageInstance::parse(&hdr).unwrap();
290 assert!(mi.recipe.is_some());
291 }
292
293 #[test]
294 fn signature_m_can_reach_u32_max() {
295 let v: &[u8] =
296 b"i=1; m=4294967295; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=donotmodify;";
297 let sig = Signature::parse(v).unwrap();
298 assert_eq!(sig.m, u32::MAX);
299 }
300
301 #[test]
302 fn signature_parse_decodes_null_reverse_path() {
303 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=PD4=; rt=Yg==; s=sel:rsa-sha256:QQ==;";
304 let sig = Signature::parse(v).unwrap();
305 if let ChainBinding::Envelope { mail_from, .. } = &sig.chain {
306 assert_eq!(mail_from, "<>");
307 } else {
308 panic!("expected envelope chain");
309 }
310 }
311
312 #[test]
313 fn nonce_over_64_chars_is_rejected() {
314 let long = "a".repeat(65);
315 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; n={long}; s=sel:rsa-sha256:QQ==;");
316 assert!(matches!(
317 Signature::parse(v.as_bytes()),
318 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
319 ));
320 }
321
322 #[test]
323 fn nonce_exactly_64_chars_is_accepted() {
324 let n = "a".repeat(64);
325 let v = format!("i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; n={n}; s=sel:rsa-sha256:QQ==;");
326 let sig = Signature::parse(v.as_bytes()).unwrap();
327 assert_eq!(sig.n.as_deref(), Some(n.as_str()));
328 }
329
330 #[test]
331 fn duplicate_signature_tag_is_rejected() {
332 let v: &[u8] = b"i=1; i=2; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
333 assert!(matches!(
334 Signature::parse(v),
335 Err(Error::Dkim2(Dkim2Error::SignatureSyntax(_)))
336 ));
337 }
338
339 #[test]
340 fn duplicate_signature_tag_case_insensitive_is_rejected() {
341 let v: &[u8] = b"i=1; m=1; M=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
342 assert!(Signature::parse(v).is_err());
343 }
344
345 #[test]
346 fn duplicate_message_instance_tag_is_rejected() {
347 let v: &[u8] = b"m=1; m=2; h=sha256:QQ==:Qg==;";
348 assert!(matches!(
349 MessageInstance::parse(v),
350 Err(Error::Dkim2(Dkim2Error::InstanceSyntax(_)))
351 ));
352 }
353
354 #[test]
355 fn missing_t_tag_is_rejected() {
356 let v: &[u8] = b"i=1; m=1; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
357 assert!(matches!(
358 Signature::parse(v),
359 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
360 tag: "t",
361 ..
362 }))
363 ));
364 }
365
366 #[test]
367 fn missing_s_tag_is_rejected() {
368 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==;";
369 assert!(matches!(
370 Signature::parse(v),
371 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
372 tag: "s",
373 ..
374 }))
375 ));
376 }
377
378 #[test]
379 fn nd_with_envelope_is_rejected_as_unexpected() {
380 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; nd=next.example; mf=YQ==; s=sel:rsa-sha256:QQ==;";
381 assert!(matches!(
382 Signature::parse(v),
383 Err(Error::Dkim2(Dkim2Error::SignatureTagUnexpected {
384 tag: "nd",
385 ..
386 }))
387 ));
388 }
389
390 #[test]
391 fn unknown_algorithm_in_s_is_ignored() {
392 let v: &[u8] =
393 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:,sel:ed25519-sha256:QQ==;";
394 let sig = Signature::parse(v).unwrap();
395 assert_eq!(sig.s.len(), 1);
396 assert_eq!(sig.s[0].selector, "sel");
397 }
398
399 #[test]
400 fn only_unknown_algorithms_yields_empty_s() {
401 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=banana:banana:;";
402 let sig = Signature::parse(v).unwrap();
403 assert!(sig.s.is_empty());
404 }
405
406 #[test]
407 fn mf_without_rt_is_rejected() {
408 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; s=sel:rsa-sha256:QQ==;";
409 assert!(matches!(
410 Signature::parse(v),
411 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
412 tag: "rt",
413 ..
414 }))
415 ));
416 }
417
418 #[test]
419 fn rt_without_mf_is_rejected() {
420 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; rt=Yg==; s=sel:rsa-sha256:QQ==;";
421 assert!(matches!(
422 Signature::parse(v),
423 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
424 tag: "mf",
425 ..
426 }))
427 ));
428 }
429
430 #[test]
431 fn missing_d_tag_is_rejected() {
432 let v: &[u8] = b"i=1; m=1; t=0; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==;";
433 assert!(matches!(
434 Signature::parse(v),
435 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
436 tag: "d",
437 ..
438 }))
439 ));
440 }
441
442 #[test]
443 fn neither_nd_nor_envelope_is_rejected() {
444 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; s=sel:rsa-sha256:QQ==;";
445 assert!(matches!(
446 Signature::parse(v),
447 Err(Error::Dkim2(Dkim2Error::SignatureTagMissing {
448 tag: "mf",
449 ..
450 }))
451 ));
452 }
453
454 #[test]
455 fn rsa_sha1_set_is_dropped() {
456 let v: &[u8] =
457 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==,sel2:ed25519-sha256:QQ==;";
458 let sig = Signature::parse(v).unwrap();
459 assert_eq!(sig.s.len(), 1);
460 assert_eq!(sig.s[0].selector, "sel2");
461 }
462
463 #[test]
464 fn only_rsa_sha1_yields_empty_s() {
465 let v: &[u8] = b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha1:QQ==;";
466 let sig = Signature::parse(v).unwrap();
467 assert!(sig.s.is_empty());
468 }
469
470 #[test]
471 fn feedhere_flag_is_parsed() {
472 let v: &[u8] =
473 b"i=1; m=1; t=0; d=ex.com; mf=YQ==; rt=Yg==; s=sel:rsa-sha256:QQ==; f=feedback,feedhere;";
474 let sig = Signature::parse(v).unwrap();
475 assert_eq!(sig.flags, vec![Flag::Feedback, Flag::FeedHere]);
476 }
477}