1use std::collections::HashSet;
2
3use base64::Engine;
4
5use super::types::{Algorithm, CanonicalizationMethod, DkimSignature, PermFailKind};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DkimParseError {
10 pub kind: PermFailKind,
11 pub detail: String,
12}
13
14impl std::fmt::Display for DkimParseError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 write!(f, "{:?}: {}", self.kind, self.detail)
17 }
18}
19
20impl std::error::Error for DkimParseError {}
21
22fn malformed(detail: impl Into<String>) -> DkimParseError {
23 DkimParseError {
24 kind: PermFailKind::MalformedSignature,
25 detail: detail.into(),
26 }
27}
28
29fn domain_mismatch(detail: impl Into<String>) -> DkimParseError {
30 DkimParseError {
31 kind: PermFailKind::DomainMismatch,
32 detail: detail.into(),
33 }
34}
35
36pub fn parse_tag_list(input: &str) -> Vec<(String, String)> {
39 let unfolded = unfold(input);
41
42 let mut tags = Vec::new();
43 for part in unfolded.split(';') {
44 let trimmed = part.trim();
45 if trimmed.is_empty() {
46 continue;
47 }
48 if let Some((name, value)) = trimmed.split_once('=') {
49 tags.push((name.trim().to_string(), value.trim().to_string()));
50 }
51 }
52 tags
53}
54
55fn unfold(s: &str) -> String {
57 let mut result = String::with_capacity(s.len());
58 let bytes = s.as_bytes();
59 let mut i = 0;
60 while i < bytes.len() {
61 if i + 1 < bytes.len() && bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
62 if i + 2 < bytes.len() && (bytes[i + 2] == b' ' || bytes[i + 2] == b'\t') {
64 i += 2;
66 continue;
67 }
68 }
69 result.push(bytes[i] as char);
70 i += 1;
71 }
72 result
73}
74
75fn decode_base64(value: &str) -> Result<Vec<u8>, DkimParseError> {
77 let cleaned: String = value.chars().filter(|c| !c.is_ascii_whitespace()).collect();
78 base64::engine::general_purpose::STANDARD
79 .decode(&cleaned)
80 .map_err(|e| malformed(format!("invalid base64: {}", e)))
81}
82
83impl DkimSignature {
84 pub fn parse(header_value: &str) -> Result<Self, DkimParseError> {
87 let raw_header = header_value.to_string();
88 let tags = parse_tag_list(header_value);
89
90 let mut seen = HashSet::new();
92 for (name, _) in &tags {
93 if !seen.insert(name.as_str()) {
94 return Err(malformed(format!("duplicate tag: {}", name)));
95 }
96 }
97
98 let get = |name: &str| -> Option<&str> {
99 tags.iter()
100 .find(|(n, _)| n == name)
101 .map(|(_, v)| v.as_str())
102 };
103
104 let version_str = get("v").ok_or_else(|| malformed("missing required tag: v"))?;
106 let version: u8 = version_str
107 .parse()
108 .map_err(|_| malformed(format!("invalid version: {}", version_str)))?;
109 if version != 1 {
110 return Err(malformed(format!("unsupported version: {}", version)));
111 }
112
113 let algo_str = get("a").ok_or_else(|| malformed("missing required tag: a"))?;
114 let algorithm = Algorithm::parse(algo_str)
115 .ok_or_else(|| malformed(format!("unknown algorithm: {}", algo_str)))?;
116
117 let b_raw = get("b").ok_or_else(|| malformed("missing required tag: b"))?;
118 let signature = decode_base64(b_raw)?;
119
120 let bh_raw = get("bh").ok_or_else(|| malformed("missing required tag: bh"))?;
121 let body_hash = decode_base64(bh_raw)?;
122
123 let domain = get("d")
124 .ok_or_else(|| malformed("missing required tag: d"))?
125 .to_string();
126
127 let h_raw = get("h").ok_or_else(|| malformed("missing required tag: h"))?;
128 let signed_headers: Vec<String> = h_raw
129 .split(':')
130 .map(|s| s.trim().to_string())
131 .filter(|s| !s.is_empty())
132 .collect();
133
134 if !signed_headers.iter().any(|h| h.eq_ignore_ascii_case("from")) {
136 return Err(malformed("h= tag must include \"from\""));
137 }
138
139 let selector = get("s")
140 .ok_or_else(|| malformed("missing required tag: s"))?
141 .to_string();
142
143 let (header_canonicalization, body_canonicalization) = if let Some(c_val) = get("c") {
145 parse_canonicalization(c_val)?
146 } else {
147 (CanonicalizationMethod::Simple, CanonicalizationMethod::Simple)
148 };
149
150 let auid = if let Some(i_val) = get("i") {
151 i_val.to_string()
152 } else {
153 format!("@{}", domain)
154 };
155
156 validate_auid_domain(&auid, &domain)?;
158
159 let body_length = if let Some(l_val) = get("l") {
160 Some(
161 l_val
162 .parse::<u64>()
163 .map_err(|_| malformed(format!("invalid l= value: {}", l_val)))?,
164 )
165 } else {
166 None
167 };
168
169 let timestamp = if let Some(t_val) = get("t") {
173 Some(
174 t_val
175 .parse::<u64>()
176 .map_err(|_| malformed(format!("invalid t= value: {}", t_val)))?,
177 )
178 } else {
179 None
180 };
181
182 let expiration = if let Some(x_val) = get("x") {
183 Some(
184 x_val
185 .parse::<u64>()
186 .map_err(|_| malformed(format!("invalid x= value: {}", x_val)))?,
187 )
188 } else {
189 None
190 };
191
192 let copied_headers = get("z").map(|z_val| {
193 z_val
194 .split('|')
195 .map(|s| s.trim().to_string())
196 .collect::<Vec<_>>()
197 });
198
199 Ok(DkimSignature {
200 version,
201 algorithm,
202 signature,
203 body_hash,
204 header_canonicalization,
205 body_canonicalization,
206 domain,
207 signed_headers,
208 auid,
209 body_length,
210 selector,
211 timestamp,
212 expiration,
213 copied_headers,
214 raw_header,
215 })
216 }
217}
218
219fn parse_canonicalization(
221 value: &str,
222) -> Result<(CanonicalizationMethod, CanonicalizationMethod), DkimParseError> {
223 if let Some((header, body)) = value.split_once('/') {
224 let h = CanonicalizationMethod::parse(header.trim())
225 .ok_or_else(|| malformed(format!("unknown header canonicalization: {}", header)))?;
226 let b = CanonicalizationMethod::parse(body.trim())
227 .ok_or_else(|| malformed(format!("unknown body canonicalization: {}", body)))?;
228 Ok((h, b))
229 } else {
230 let h = CanonicalizationMethod::parse(value.trim())
231 .ok_or_else(|| malformed(format!("unknown canonicalization: {}", value)))?;
232 Ok((h, CanonicalizationMethod::Simple))
234 }
235}
236
237fn validate_auid_domain(auid: &str, domain: &str) -> Result<(), DkimParseError> {
239 let i_domain = if let Some(at_pos) = auid.rfind('@') {
241 &auid[at_pos + 1..]
242 } else {
243 auid
244 };
245
246 let i_lower = i_domain.to_ascii_lowercase();
247 let d_lower = domain.to_ascii_lowercase();
248
249 if i_lower == d_lower {
250 return Ok(());
251 }
252
253 if i_lower.ends_with(&format!(".{}", d_lower)) {
255 return Ok(());
256 }
257
258 Err(domain_mismatch(format!(
259 "i= domain '{}' is not subdomain of d= '{}'",
260 i_domain, domain
261 )))
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::dkim::types::*;
268
269 fn minimal_sig() -> String {
271 let b = base64::engine::general_purpose::STANDARD.encode(b"fakesig");
272 let bh = base64::engine::general_purpose::STANDARD.encode(b"fakehash");
273 format!(
274 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
275 b, bh
276 )
277 }
278
279 #[test]
281 fn parse_minimal_signature() {
282 let sig = DkimSignature::parse(&minimal_sig()).unwrap();
283 assert_eq!(sig.version, 1);
284 assert_eq!(sig.algorithm, Algorithm::RsaSha256);
285 assert_eq!(sig.domain, "example.com");
286 assert_eq!(sig.selector, "sel1");
287 assert_eq!(sig.signed_headers, vec!["from"]);
288 assert_eq!(sig.auid, "@example.com"); assert_eq!(
290 sig.header_canonicalization,
291 CanonicalizationMethod::Simple
292 );
293 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Simple);
294 assert!(sig.body_length.is_none());
295 assert!(sig.timestamp.is_none());
296 assert!(sig.expiration.is_none());
297 assert!(sig.copied_headers.is_none());
298 }
299
300 #[test]
302 fn signature_has_all_fields() {
303 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
304 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
305 let input = format!(
306 "v=1; a=ed25519-sha256; b={}; bh={}; d=example.com; h=from:to:subject; \
307 s=sel1; c=relaxed/relaxed; i=user@example.com; l=100; t=1000; x=2000; \
308 z=From:user@example.com|To:dest@example.com",
309 b, bh
310 );
311 let sig = DkimSignature::parse(&input).unwrap();
312 assert_eq!(sig.version, 1);
313 assert_eq!(sig.algorithm, Algorithm::Ed25519Sha256);
314 assert_eq!(sig.signature, b"sig");
315 assert_eq!(sig.body_hash, b"hash");
316 assert_eq!(
317 sig.header_canonicalization,
318 CanonicalizationMethod::Relaxed
319 );
320 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Relaxed);
321 assert_eq!(sig.domain, "example.com");
322 assert_eq!(sig.signed_headers, vec!["from", "to", "subject"]);
323 assert_eq!(sig.auid, "user@example.com");
324 assert_eq!(sig.body_length, Some(100));
325 assert_eq!(sig.selector, "sel1");
326 assert_eq!(sig.timestamp, Some(1000));
327 assert_eq!(sig.expiration, Some(2000));
328 assert_eq!(
329 sig.copied_headers,
330 Some(vec![
331 "From:user@example.com".to_string(),
332 "To:dest@example.com".to_string()
333 ])
334 );
335 }
336
337 #[test]
339 fn parse_all_optional_tags() {
340 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
341 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
342 let input = format!(
343 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; \
344 c=relaxed/simple; i=user@sub.example.com; l=500; q=dns/txt; t=12345; x=99999; \
345 z=From:test",
346 b, bh
347 );
348 let sig = DkimSignature::parse(&input).unwrap();
349 assert_eq!(
350 sig.header_canonicalization,
351 CanonicalizationMethod::Relaxed
352 );
353 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Simple);
354 assert_eq!(sig.auid, "user@sub.example.com");
355 assert_eq!(sig.body_length, Some(500));
356 assert_eq!(sig.timestamp, Some(12345));
357 assert_eq!(sig.expiration, Some(99999));
358 assert_eq!(sig.copied_headers, Some(vec!["From:test".to_string()]));
359 }
360
361 #[test]
363 fn parse_folded_header() {
364 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
365 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
366 let input = format!(
367 "v=1; a=rsa-sha256;\r\n b={};\r\n\tbh={}; d=example.com;\r\n h=from; s=sel1",
368 b, bh
369 );
370 let sig = DkimSignature::parse(&input).unwrap();
371 assert_eq!(sig.algorithm, Algorithm::RsaSha256);
372 assert_eq!(sig.domain, "example.com");
373 }
374
375 #[test]
377 fn parse_base64_with_whitespace() {
378 let raw_b = base64::engine::general_purpose::STANDARD.encode(b"signaturedata");
379 let raw_bh = base64::engine::general_purpose::STANDARD.encode(b"bodyhashdata");
380 let spaced_b = format!(
382 "{} {}",
383 &raw_b[..raw_b.len() / 2],
384 &raw_b[raw_b.len() / 2..]
385 );
386 let input = format!(
387 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
388 spaced_b, raw_bh
389 );
390 let sig = DkimSignature::parse(&input).unwrap();
391 assert_eq!(sig.signature, b"signaturedata");
392 }
393
394 #[test]
396 fn parse_missing_required_tag_v() {
397 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
398 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
399 let input = format!(
400 "a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
401 b, bh
402 );
403 let err = DkimSignature::parse(&input).unwrap_err();
404 assert_eq!(err.kind, PermFailKind::MalformedSignature);
405 assert!(err.detail.contains("v"));
406 }
407
408 #[test]
409 fn parse_missing_required_tag_b() {
410 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
411 let input = format!(
412 "v=1; a=rsa-sha256; bh={}; d=example.com; h=from; s=sel1",
413 bh
414 );
415 let err = DkimSignature::parse(&input).unwrap_err();
416 assert_eq!(err.kind, PermFailKind::MalformedSignature);
417 }
418
419 #[test]
420 fn parse_missing_required_tag_d() {
421 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
422 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
423 let input = format!("v=1; a=rsa-sha256; b={}; bh={}; h=from; s=sel1", b, bh);
424 let err = DkimSignature::parse(&input).unwrap_err();
425 assert_eq!(err.kind, PermFailKind::MalformedSignature);
426 assert!(err.detail.contains("d"));
427 }
428
429 #[test]
430 fn parse_missing_required_tag_h() {
431 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
432 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
433 let input = format!(
434 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; s=sel1",
435 b, bh
436 );
437 let err = DkimSignature::parse(&input).unwrap_err();
438 assert_eq!(err.kind, PermFailKind::MalformedSignature);
439 assert!(err.detail.contains("h"));
440 }
441
442 #[test]
443 fn parse_missing_required_tag_s() {
444 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
445 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
446 let input = format!(
447 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from",
448 b, bh
449 );
450 let err = DkimSignature::parse(&input).unwrap_err();
451 assert_eq!(err.kind, PermFailKind::MalformedSignature);
452 assert!(err.detail.contains("s"));
453 }
454
455 #[test]
457 fn parse_duplicate_tag() {
458 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
459 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
460 let input = format!(
461 "v=1; v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
462 b, bh
463 );
464 let err = DkimSignature::parse(&input).unwrap_err();
465 assert_eq!(err.kind, PermFailKind::MalformedSignature);
466 assert!(err.detail.contains("duplicate"));
467 }
468
469 #[test]
471 fn parse_unknown_tag_ignored() {
472 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
473 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
474 let input = format!(
475 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; x_custom=hello",
476 b, bh
477 );
478 let sig = DkimSignature::parse(&input).unwrap();
479 assert_eq!(sig.domain, "example.com");
480 }
481
482 #[test]
484 fn parse_invalid_algorithm() {
485 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
486 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
487 let input = format!(
488 "v=1; a=rsa-md5; b={}; bh={}; d=example.com; h=from; s=sel1",
489 b, bh
490 );
491 let err = DkimSignature::parse(&input).unwrap_err();
492 assert_eq!(err.kind, PermFailKind::MalformedSignature);
493 assert!(err.detail.contains("unknown algorithm"));
494 }
495
496 #[test]
498 fn parse_case_insensitive_algorithm() {
499 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
500 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
501 let input = format!(
502 "v=1; a=RSA-SHA256; b={}; bh={}; d=example.com; h=from; s=sel1",
503 b, bh
504 );
505 let sig = DkimSignature::parse(&input).unwrap();
506 assert_eq!(sig.algorithm, Algorithm::RsaSha256);
507 }
508
509 #[test]
511 fn parse_h_missing_from() {
512 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
513 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
514 let input = format!(
515 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=to:subject; s=sel1",
516 b, bh
517 );
518 let err = DkimSignature::parse(&input).unwrap_err();
519 assert_eq!(err.kind, PermFailKind::MalformedSignature);
520 assert!(err.detail.contains("from"));
521 }
522
523 #[test]
525 fn parse_i_not_subdomain() {
526 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
527 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
528 let input = format!(
529 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; i=user@other.com",
530 b, bh
531 );
532 let err = DkimSignature::parse(&input).unwrap_err();
533 assert_eq!(err.kind, PermFailKind::DomainMismatch);
534 }
535
536 #[test]
538 fn parse_c_relaxed_relaxed() {
539 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
540 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
541 let input = format!(
542 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; c=relaxed/relaxed",
543 b, bh
544 );
545 let sig = DkimSignature::parse(&input).unwrap();
546 assert_eq!(
547 sig.header_canonicalization,
548 CanonicalizationMethod::Relaxed
549 );
550 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Relaxed);
551 }
552
553 #[test]
554 fn parse_c_simple_only() {
555 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
556 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
557 let input = format!(
558 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; c=simple",
559 b, bh
560 );
561 let sig = DkimSignature::parse(&input).unwrap();
562 assert_eq!(sig.header_canonicalization, CanonicalizationMethod::Simple);
563 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Simple);
564 }
565
566 #[test]
567 fn parse_c_relaxed_only_body_defaults_simple() {
568 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
569 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
570 let input = format!(
571 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; c=relaxed",
572 b, bh
573 );
574 let sig = DkimSignature::parse(&input).unwrap();
575 assert_eq!(
576 sig.header_canonicalization,
577 CanonicalizationMethod::Relaxed
578 );
579 assert_eq!(sig.body_canonicalization, CanonicalizationMethod::Simple);
580 }
581
582 #[test]
584 fn algorithm_parse_all_variants() {
585 assert_eq!(Algorithm::parse("rsa-sha1"), Some(Algorithm::RsaSha1));
586 assert_eq!(Algorithm::parse("rsa-sha256"), Some(Algorithm::RsaSha256));
587 assert_eq!(
588 Algorithm::parse("ed25519-sha256"),
589 Some(Algorithm::Ed25519Sha256)
590 );
591 assert_eq!(Algorithm::parse("RSA-SHA256"), Some(Algorithm::RsaSha256));
592 assert!(Algorithm::parse("unknown").is_none());
593 }
594
595 #[test]
597 fn canonicalization_parse() {
598 assert_eq!(
599 CanonicalizationMethod::parse("simple"),
600 Some(CanonicalizationMethod::Simple)
601 );
602 assert_eq!(
603 CanonicalizationMethod::parse("relaxed"),
604 Some(CanonicalizationMethod::Relaxed)
605 );
606 assert_eq!(
607 CanonicalizationMethod::parse("SIMPLE"),
608 Some(CanonicalizationMethod::Simple)
609 );
610 assert!(CanonicalizationMethod::parse("unknown").is_none());
611 }
612
613 #[test]
615 fn result_types_exist() {
616 let _pass = DkimResult::Pass {
617 domain: "example.com".into(),
618 selector: "sel1".into(),
619 testing: false,
620 };
621 let _fail = DkimResult::Fail {
622 kind: FailureKind::BodyHashMismatch,
623 detail: "test".into(),
624 };
625 let _permfail = DkimResult::PermFail {
626 kind: PermFailKind::MalformedSignature,
627 detail: "test".into(),
628 };
629 let _tempfail = DkimResult::TempFail {
630 reason: "dns".into(),
631 };
632 let _none = DkimResult::None;
633
634 let _ = FailureKind::BodyHashMismatch;
636 let _ = FailureKind::SignatureVerificationFailed;
637
638 let _ = PermFailKind::MalformedSignature;
640 let _ = PermFailKind::KeyRevoked;
641 let _ = PermFailKind::KeyNotFound;
642 let _ = PermFailKind::ExpiredSignature;
643 let _ = PermFailKind::AlgorithmMismatch;
644 let _ = PermFailKind::HashNotPermitted;
645 let _ = PermFailKind::ServiceTypeMismatch;
646 let _ = PermFailKind::StrictModeViolation;
647 let _ = PermFailKind::DomainMismatch;
648 }
649
650 #[test]
652 fn tag_list_parsing() {
653 let tags = parse_tag_list("a=b; c=d; e=f");
654 assert_eq!(tags.len(), 3);
655 assert_eq!(tags[0], ("a".into(), "b".into()));
656 assert_eq!(tags[1], ("c".into(), "d".into()));
657 assert_eq!(tags[2], ("e".into(), "f".into()));
658 }
659
660 #[test]
662 fn unfold_crlf_space() {
663 let input = "hello\r\n world";
664 let result = unfold(input);
665 assert_eq!(result, "hello world");
666 }
667
668 #[test]
669 fn unfold_crlf_tab() {
670 let input = "hello\r\n\tworld";
671 let result = unfold(input);
672 assert_eq!(result, "hello\tworld");
673 }
674
675 #[test]
677 fn tag_list_strips_whitespace() {
678 let tags = parse_tag_list(" a = b ; c = d ");
679 assert_eq!(tags[0], ("a".into(), "b".into()));
680 assert_eq!(tags[1], ("c".into(), "d".into()));
681 }
682
683 #[test]
685 fn decode_base64_with_spaces() {
686 let encoded = base64::engine::general_purpose::STANDARD.encode(b"test data");
687 let spaced = format!("{} {}", &encoded[..4], &encoded[4..]);
688 let decoded = decode_base64(&spaced).unwrap();
689 assert_eq!(decoded, b"test data");
690 }
691
692 #[test]
694 fn parse_version_not_1() {
695 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
696 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
697 let input = format!(
698 "v=2; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
699 b, bh
700 );
701 let err = DkimSignature::parse(&input).unwrap_err();
702 assert!(err.detail.contains("version"));
703 }
704
705 #[test]
707 fn parse_i_defaults_to_at_domain() {
708 let sig = DkimSignature::parse(&minimal_sig()).unwrap();
709 assert_eq!(sig.auid, "@example.com");
710 }
711
712 #[test]
714 fn parse_i_subdomain_valid() {
715 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
716 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
717 let input = format!(
718 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; i=user@sub.example.com",
719 b, bh
720 );
721 let sig = DkimSignature::parse(&input).unwrap();
722 assert_eq!(sig.auid, "user@sub.example.com");
723 }
724
725 #[test]
727 fn parse_stores_raw_header() {
728 let input = minimal_sig();
729 let sig = DkimSignature::parse(&input).unwrap();
730 assert_eq!(sig.raw_header, input);
731 }
732
733 #[test]
735 fn parse_h_multiple_headers() {
736 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
737 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
738 let input = format!(
739 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from:to:subject:date; s=sel1",
740 b, bh
741 );
742 let sig = DkimSignature::parse(&input).unwrap();
743 assert_eq!(
744 sig.signed_headers,
745 vec!["from", "to", "subject", "date"]
746 );
747 }
748
749 #[test]
751 fn parse_z_copied_headers() {
752 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
753 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
754 let input = format!(
755 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; z=From:a|To:b|Cc:c",
756 b, bh
757 );
758 let sig = DkimSignature::parse(&input).unwrap();
759 assert_eq!(
760 sig.copied_headers,
761 Some(vec![
762 "From:a".to_string(),
763 "To:b".to_string(),
764 "Cc:c".to_string()
765 ])
766 );
767 }
768
769 #[test]
771 fn all_types_are_typed_enums() {
772 let a = Algorithm::RsaSha256;
774 assert_eq!(a.hash_algorithm(), HashAlgorithm::Sha256);
775 let a = Algorithm::RsaSha1;
776 assert_eq!(a.hash_algorithm(), HashAlgorithm::Sha1);
777
778 let _ = CanonicalizationMethod::Simple;
780 let _ = CanonicalizationMethod::Relaxed;
781
782 let _ = KeyType::Rsa;
784 let _ = KeyType::Ed25519;
785
786 let _ = HashAlgorithm::Sha1;
788 let _ = HashAlgorithm::Sha256;
789
790 let _ = KeyFlag::Testing;
792 let _ = KeyFlag::Strict;
793 }
794
795 #[test]
797 fn parse_rsa_sha1_signature() {
798 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
799 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
800 let input = format!(
801 "v=1; a=rsa-sha1; b={}; bh={}; d=example.com; h=from; s=sel1",
802 b, bh
803 );
804 let sig = DkimSignature::parse(&input).unwrap();
805 assert_eq!(sig.algorithm, Algorithm::RsaSha1);
806 }
807
808 #[test]
809 fn parse_ed25519_signature() {
810 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
811 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
812 let input = format!(
813 "v=1; a=ed25519-sha256; b={}; bh={}; d=example.com; h=from; s=sel1",
814 b, bh
815 );
816 let sig = DkimSignature::parse(&input).unwrap();
817 assert_eq!(sig.algorithm, Algorithm::Ed25519Sha256);
818 }
819
820 #[test]
822 fn parse_missing_bh() {
823 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
824 let input = format!(
825 "v=1; a=rsa-sha256; b={}; d=example.com; h=from; s=sel1",
826 b
827 );
828 let err = DkimSignature::parse(&input).unwrap_err();
829 assert_eq!(err.kind, PermFailKind::MalformedSignature);
830 assert!(err.detail.contains("bh"));
831 }
832
833 #[test]
835 fn parse_duplicate_d_tag() {
836 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
837 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
838 let input = format!(
839 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; d=other.com; h=from; s=sel1",
840 b, bh
841 );
842 let err = DkimSignature::parse(&input).unwrap_err();
843 assert_eq!(err.kind, PermFailKind::MalformedSignature);
844 assert!(err.detail.contains("duplicate"));
845 }
846
847 #[test]
849 fn parse_h_from_case_insensitive() {
850 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
851 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
852 let input = format!(
853 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=From:To; s=sel1",
854 b, bh
855 );
856 let sig = DkimSignature::parse(&input).unwrap();
857 assert_eq!(sig.signed_headers[0], "From");
858 }
859
860 #[test]
862 fn validate_auid_domain_equal() {
863 assert!(validate_auid_domain("user@example.com", "example.com").is_ok());
864 }
865
866 #[test]
867 fn validate_auid_domain_subdomain() {
868 assert!(validate_auid_domain("user@sub.example.com", "example.com").is_ok());
869 }
870
871 #[test]
872 fn validate_auid_domain_different() {
873 assert!(validate_auid_domain("user@other.com", "example.com").is_err());
874 }
875
876 #[test]
877 fn validate_auid_domain_case_insensitive() {
878 assert!(validate_auid_domain("user@EXAMPLE.COM", "example.com").is_ok());
879 }
880
881 #[test]
883 fn parse_l_body_length() {
884 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
885 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
886 let input = format!(
887 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; l=12345",
888 b, bh
889 );
890 let sig = DkimSignature::parse(&input).unwrap();
891 assert_eq!(sig.body_length, Some(12345));
892 }
893
894 #[test]
896 fn parse_timestamp_expiration() {
897 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
898 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
899 let input = format!(
900 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; t=1000000; x=2000000",
901 b, bh
902 );
903 let sig = DkimSignature::parse(&input).unwrap();
904 assert_eq!(sig.timestamp, Some(1000000));
905 assert_eq!(sig.expiration, Some(2000000));
906 }
907
908 #[test]
910 fn parse_multiple_unknown_tags() {
911 let b = base64::engine::general_purpose::STANDARD.encode(b"sig");
912 let bh = base64::engine::general_purpose::STANDARD.encode(b"hash");
913 let input = format!(
914 "v=1; a=rsa-sha256; b={}; bh={}; d=example.com; h=from; s=sel1; foo=bar; baz=qux",
915 b, bh
916 );
917 let sig = DkimSignature::parse(&input).unwrap();
918 assert_eq!(sig.selector, "sel1");
919 }
920}