1use std::time::{SystemTime, UNIX_EPOCH};
2
3use ring::signature as ring_sig;
4use subtle::ConstantTimeEq;
5
6use crate::common::dns::{DnsError, DnsResolver};
7
8use super::canon::{
9 apply_body_length_limit, canonicalize_body, canonicalize_header, normalize_line_endings,
10 select_headers, strip_b_tag_value,
11};
12use super::key::DkimPublicKey;
13use super::types::{
14 Algorithm, CanonicalizationMethod, DkimResult, DkimSignature, FailureKind, HashAlgorithm,
15 KeyType, PermFailKind,
16};
17
18pub struct DkimVerifier<R: DnsResolver> {
20 resolver: R,
21 clock_skew: u64, }
23
24impl<R: DnsResolver> DkimVerifier<R> {
25 pub fn new(resolver: R) -> Self {
26 Self {
27 resolver,
28 clock_skew: 300,
29 }
30 }
31
32 pub fn clock_skew(mut self, seconds: u64) -> Self {
33 self.clock_skew = seconds;
34 self
35 }
36
37 pub async fn verify_message(
42 &self,
43 headers: &[(&str, &str)],
44 body: &[u8],
45 ) -> Vec<DkimResult> {
46 let dkim_indices: Vec<usize> = headers
48 .iter()
49 .enumerate()
50 .filter(|(_, (name, _))| name.eq_ignore_ascii_case("dkim-signature"))
51 .map(|(i, _)| i)
52 .collect();
53
54 if dkim_indices.is_empty() {
55 return vec![DkimResult::None];
56 }
57
58 let mut results = Vec::new();
59 for idx in dkim_indices {
60 let (_, value) = headers[idx];
61 let result = self.verify_single(headers, body, value, idx).await;
62 results.push(result);
63 }
64 results
65 }
66
67 async fn verify_single(
68 &self,
69 headers: &[(&str, &str)],
70 body: &[u8],
71 sig_value: &str,
72 sig_idx: usize,
73 ) -> DkimResult {
74 let sig = match DkimSignature::parse(sig_value) {
76 Ok(s) => s,
77 Err(e) => {
78 return DkimResult::PermFail {
79 kind: e.kind,
80 detail: e.detail,
81 }
82 }
83 };
84
85 if let (Some(t), Some(x)) = (sig.timestamp, sig.expiration) {
87 if x < t {
88 return DkimResult::PermFail {
89 kind: PermFailKind::MalformedSignature,
90 detail: format!("x= ({}) must be >= t= ({}) per RFC 6376 §3.5", x, t),
91 };
92 }
93 }
94
95 if let Some(expiration) = sig.expiration {
97 let now = current_timestamp();
98 if now > expiration + self.clock_skew {
99 return DkimResult::PermFail {
100 kind: PermFailKind::ExpiredSignature,
101 detail: format!("signature expired at {}, now {}", expiration, now),
102 };
103 }
104 }
105
106 let query = format!("{}._domainkey.{}", sig.selector, sig.domain);
108 let key = match self.lookup_key(&query).await {
109 Ok(k) => k,
110 Err(result) => return result,
111 };
112
113 if let Some(result) = enforce_key_constraints(&sig, &key) {
115 return result;
116 }
117
118 if let Some(result) =
120 verify_body_hash(&sig, body)
121 {
122 return result;
123 }
124
125 let header_data = compute_header_hash_input(&sig, headers, sig_idx);
127
128 match verify_signature(&sig.algorithm, &key, &header_data, &sig.signature) {
130 Ok(()) => DkimResult::Pass {
131 domain: sig.domain,
132 selector: sig.selector,
133 testing: key.is_testing(),
134 },
135 Err(detail) => DkimResult::Fail {
136 kind: FailureKind::SignatureVerificationFailed,
137 detail,
138 },
139 }
140 }
141
142 async fn lookup_key(&self, query: &str) -> Result<DkimPublicKey, DkimResult> {
143 let txt_records = match self.resolver.query_txt(query).await {
144 Ok(records) => records,
145 Err(DnsError::NxDomain) | Err(DnsError::NoRecords) => {
146 return Err(DkimResult::PermFail {
147 kind: PermFailKind::KeyNotFound,
148 detail: format!("no DNS key record at {}", query),
149 })
150 }
151 Err(DnsError::TempFail) => {
152 return Err(DkimResult::TempFail {
153 reason: format!("DNS temp failure for {}", query),
154 })
155 }
156 };
157
158 let concatenated = txt_records.join("");
160
161 DkimPublicKey::parse(&concatenated).map_err(|e| DkimResult::PermFail {
162 kind: e.kind,
163 detail: e.detail,
164 })
165 }
166}
167
168fn current_timestamp() -> u64 {
169 SystemTime::now()
170 .duration_since(UNIX_EPOCH)
171 .map(|d| d.as_secs())
172 .unwrap_or(0)
173}
174
175pub(crate) fn enforce_key_constraints(sig: &DkimSignature, key: &DkimPublicKey) -> Option<DkimResult> {
177 if key.revoked {
179 return Some(DkimResult::PermFail {
180 kind: PermFailKind::KeyRevoked,
181 detail: "key revoked (empty p= tag)".into(),
182 });
183 }
184
185 if let Some(ref hash_algs) = key.hash_algorithms {
187 let sig_hash = sig.algorithm.hash_algorithm();
188 if !hash_algs.contains(&sig_hash) {
189 return Some(DkimResult::PermFail {
190 kind: PermFailKind::HashNotPermitted,
191 detail: format!("key h= tag does not permit {:?}", sig_hash),
192 });
193 }
194 }
195
196 if let Some(ref service_types) = key.service_types {
198 if !service_types.iter().any(|s| s == "email" || s == "*") {
199 return Some(DkimResult::PermFail {
200 kind: PermFailKind::ServiceTypeMismatch,
201 detail: "key s= tag does not include 'email' or '*'".into(),
202 });
203 }
204 }
205
206 if key.is_strict() {
208 let i_domain = sig
209 .auid
210 .rfind('@')
211 .map(|pos| &sig.auid[pos + 1..])
212 .unwrap_or(&sig.auid);
213 if !i_domain.eq_ignore_ascii_case(&sig.domain) {
214 return Some(DkimResult::PermFail {
215 kind: PermFailKind::StrictModeViolation,
216 detail: format!(
217 "key t=s strict mode: i= domain '{}' must exactly equal d= '{}'",
218 i_domain, sig.domain
219 ),
220 });
221 }
222 }
223
224 let expected_key_type = match sig.algorithm {
226 Algorithm::RsaSha1 | Algorithm::RsaSha256 => KeyType::Rsa,
227 Algorithm::Ed25519Sha256 => KeyType::Ed25519,
228 };
229 if key.key_type != expected_key_type {
230 return Some(DkimResult::PermFail {
231 kind: PermFailKind::AlgorithmMismatch,
232 detail: format!(
233 "key type {:?} incompatible with algorithm {:?}",
234 key.key_type, sig.algorithm
235 ),
236 });
237 }
238
239 None
240}
241
242pub(crate) fn verify_body_hash(sig: &DkimSignature, body: &[u8]) -> Option<DkimResult> {
244 let normalized = normalize_line_endings(body);
245 let canonicalized = canonicalize_body(sig.body_canonicalization, &normalized);
246 let limited = apply_body_length_limit(&canonicalized, sig.body_length);
247
248 let computed_hash = compute_hash(sig.algorithm, limited);
249
250 if computed_hash.ct_eq(&sig.body_hash).into() {
252 None } else {
254 Some(DkimResult::Fail {
255 kind: FailureKind::BodyHashMismatch,
256 detail: "computed body hash does not match bh= value".into(),
257 })
258 }
259}
260
261pub(crate) fn compute_hash(algorithm: Algorithm, data: &[u8]) -> Vec<u8> {
263 match algorithm.hash_algorithm() {
264 HashAlgorithm::Sha256 => {
265 let digest = ring::digest::digest(&ring::digest::SHA256, data);
266 digest.as_ref().to_vec()
267 }
268 HashAlgorithm::Sha1 => {
269 let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
270 digest.as_ref().to_vec()
271 }
272 }
273}
274
275fn compute_header_hash_input(
277 sig: &DkimSignature,
278 headers: &[(&str, &str)],
279 sig_idx: usize,
280) -> Vec<u8> {
281 let filtered_headers: Vec<(&str, &str)> = headers
283 .iter()
284 .enumerate()
285 .filter(|(i, _)| *i != sig_idx)
286 .map(|(_, h)| *h)
287 .collect();
288
289 let selected = select_headers(
291 sig.header_canonicalization,
292 &sig.signed_headers,
293 &filtered_headers,
294 );
295
296 let mut hash_input = Vec::new();
297 for header_line in &selected {
298 hash_input.extend_from_slice(header_line.as_bytes());
299 }
300
301 let stripped = strip_b_tag_value(&sig.raw_header);
303 let canon_sig = if sig.header_canonicalization == CanonicalizationMethod::Simple {
304 format!("DKIM-Signature:{}", stripped)
306 } else {
307 canonicalize_header(
308 sig.header_canonicalization,
309 "dkim-signature",
310 &stripped,
311 )
312 };
313
314 hash_input.extend_from_slice(canon_sig.as_bytes());
315 hash_input
318}
319
320pub(crate) fn strip_spki_wrapper(spki_der: &[u8]) -> &[u8] {
324 let rsa_oid: &[u8] = &[0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
328
329 if spki_der.len() < 24 || spki_der[0] != 0x30 {
330 return spki_der; }
332
333 if let Some(oid_pos) = spki_der
335 .windows(rsa_oid.len())
336 .position(|w| w == rsa_oid)
337 {
338 let after_oid = oid_pos + rsa_oid.len();
340 let mut pos = after_oid;
342 if pos + 1 < spki_der.len() && spki_der[pos] == 0x05 && spki_der[pos + 1] == 0x00 {
343 pos += 2;
344 }
345 if pos < spki_der.len() && spki_der[pos] == 0x03 {
347 pos += 1;
348 let (len, len_bytes) = parse_asn1_length(&spki_der[pos..]);
350 pos += len_bytes;
351 if len > 0 && pos < spki_der.len() {
352 pos += 1;
354 if pos < spki_der.len() {
356 return &spki_der[pos..];
357 }
358 }
359 }
360 }
361
362 spki_der }
364
365fn parse_asn1_length(data: &[u8]) -> (usize, usize) {
367 if data.is_empty() {
368 return (0, 0);
369 }
370 if data[0] < 0x80 {
371 (data[0] as usize, 1)
372 } else {
373 let num_bytes = (data[0] & 0x7f) as usize;
374 if num_bytes == 0 || num_bytes > 4 || data.len() < 1 + num_bytes {
375 return (0, 1);
376 }
377 let mut len: usize = 0;
378 for i in 0..num_bytes {
379 len = (len << 8) | (data[1 + i] as usize);
380 }
381 (len, 1 + num_bytes)
382 }
383}
384
385pub(crate) fn verify_signature(
387 algorithm: &Algorithm,
388 key: &DkimPublicKey,
389 data: &[u8],
390 signature: &[u8],
391) -> Result<(), String> {
392 let ring_algorithm: &dyn ring_sig::VerificationAlgorithm = match algorithm {
393 Algorithm::RsaSha256 => {
394 if key.public_key.len() >= 256 {
395 &ring_sig::RSA_PKCS1_2048_8192_SHA256
396 } else {
397 &ring_sig::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
398 }
399 }
400 Algorithm::RsaSha1 => {
401 if key.public_key.len() >= 256 {
402 &ring_sig::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY
403 } else {
404 &ring_sig::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY
405 }
406 }
407 Algorithm::Ed25519Sha256 => &ring_sig::ED25519,
408 };
409
410 let key_bytes = match key.key_type {
412 KeyType::Rsa => strip_spki_wrapper(&key.public_key),
413 KeyType::Ed25519 => &key.public_key,
414 };
415
416 let public_key = ring_sig::UnparsedPublicKey::new(ring_algorithm, key_bytes);
417 public_key
418 .verify(data, signature)
419 .map_err(|_| "cryptographic signature verification failed".to_string())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::common::dns::mock::MockResolver;
426 use base64::Engine;
427 use ring::rand::SystemRandom;
428 use ring::signature::{Ed25519KeyPair, KeyPair};
429
430 fn gen_ed25519_keypair() -> (Vec<u8>, Vec<u8>) {
432 let rng = SystemRandom::new();
433 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
434 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
435 let public_key = key_pair.public_key().as_ref().to_vec();
436 (pkcs8.as_ref().to_vec(), public_key)
437 }
438
439 fn ed25519_sign(pkcs8: &[u8], data: &[u8]) -> Vec<u8> {
441 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8).unwrap();
442 key_pair.sign(data).as_ref().to_vec()
443 }
444
445 fn compute_body_hash(
447 body: &[u8],
448 algorithm: Algorithm,
449 body_canon: CanonicalizationMethod,
450 ) -> String {
451 let normalized = normalize_line_endings(body);
452 let canonicalized = canonicalize_body(body_canon, &normalized);
453 let hash = compute_hash(algorithm, &canonicalized);
454 base64::engine::general_purpose::STANDARD.encode(&hash)
455 }
456
457 fn compute_header_input_manual(
459 headers: &[(&str, &str)],
460 signed_header_names: &[&str],
461 sig_header_value_stripped: &str,
462 header_canon: CanonicalizationMethod,
463 ) -> Vec<u8> {
464 let signed: Vec<String> = signed_header_names.iter().map(|s| s.to_string()).collect();
465 let selected = select_headers(header_canon, &signed, headers);
466
467 let mut input = Vec::new();
468 for h in &selected {
469 input.extend_from_slice(h.as_bytes());
470 }
471
472 let canon_sig =
474 canonicalize_header(header_canon, "dkim-signature", sig_header_value_stripped);
475 let canon_sig = if header_canon == CanonicalizationMethod::Simple {
476 format!("DKIM-Signature:{}", sig_header_value_stripped)
477 } else {
478 canon_sig
479 };
480 input.extend_from_slice(canon_sig.as_bytes());
481 input
482 }
483
484 fn setup_mock_key(resolver: &mut MockResolver, selector: &str, domain: &str, key_record: &str) {
485 let query = format!("{}._domainkey.{}", selector, domain);
486 resolver.add_txt(&query, vec![key_record.to_string()]);
487 }
488
489 #[tokio::test]
493 async fn no_dkim_signature_returns_none() {
494 let resolver = MockResolver::new();
495 let verifier = DkimVerifier::new(resolver);
496 let headers: Vec<(&str, &str)> = vec![
497 ("From", " user@example.com"),
498 ("Subject", " test"),
499 ];
500 let results = verifier.verify_message(&headers, b"body").await;
501 assert_eq!(results.len(), 1);
502 assert_eq!(results[0], DkimResult::None);
503 }
504
505 #[tokio::test]
508 async fn multiple_signatures_return_multiple_results() {
509 let resolver = MockResolver::new();
510 let headers: Vec<(&str, &str)> = vec![
512 ("From", " user@example.com"),
513 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=a.com; h=from; s=s1"),
514 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=b.com; h=from; s=s1"),
515 ];
516 let verifier = DkimVerifier::new(resolver);
518 let results = verifier.verify_message(&headers, b"body").await;
519 assert_eq!(results.len(), 2);
520 }
521
522 #[tokio::test]
524 async fn malformed_signature_returns_permfail() {
525 let resolver = MockResolver::new();
526 let verifier = DkimVerifier::new(resolver);
527 let headers: Vec<(&str, &str)> = vec![
528 ("From", " user@example.com"),
529 ("DKIM-Signature", " not-a-valid-signature"),
530 ];
531 let results = verifier.verify_message(&headers, b"body").await;
532 assert_eq!(results.len(), 1);
533 match &results[0] {
534 DkimResult::PermFail { kind, .. } => {
535 assert_eq!(*kind, PermFailKind::MalformedSignature);
536 }
537 _ => panic!("expected PermFail"),
538 }
539 }
540
541 #[tokio::test]
545 async fn key_not_found_nxdomain() {
546 let resolver = MockResolver::new();
547 let verifier = DkimVerifier::new(resolver);
548 let headers: Vec<(&str, &str)> = vec![
549 ("From", " user@example.com"),
550 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
551 ];
552 let results = verifier.verify_message(&headers, b"body").await;
553 match &results[0] {
554 DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::KeyNotFound),
555 _ => panic!("expected PermFail KeyNotFound"),
556 }
557 }
558
559 #[tokio::test]
561 async fn dns_temp_failure() {
562 let mut resolver = MockResolver::new();
563 resolver.add_txt_err("sel1._domainkey.example.com", DnsError::TempFail);
564 let verifier = DkimVerifier::new(resolver);
565 let headers: Vec<(&str, &str)> = vec![
566 ("From", " user@example.com"),
567 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
568 ];
569 let results = verifier.verify_message(&headers, b"body").await;
570 match &results[0] {
571 DkimResult::TempFail { .. } => {}
572 _ => panic!("expected TempFail"),
573 }
574 }
575
576 #[tokio::test]
578 async fn key_revoked_empty_p() {
579 let mut resolver = MockResolver::new();
580 setup_mock_key(&mut resolver, "sel1", "example.com", "v=DKIM1; p=");
581 let verifier = DkimVerifier::new(resolver);
582 let headers: Vec<(&str, &str)> = vec![
583 ("From", " user@example.com"),
584 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
585 ];
586 let results = verifier.verify_message(&headers, b"body").await;
587 match &results[0] {
588 DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::KeyRevoked),
589 _ => panic!("expected PermFail KeyRevoked"),
590 }
591 }
592
593 #[tokio::test]
597 async fn key_h_rejects_algorithm() {
598 let mut resolver = MockResolver::new();
599 let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
600 setup_mock_key(
601 &mut resolver,
602 "sel1",
603 "example.com",
604 &format!("v=DKIM1; h=sha1; k=rsa; p={}", p),
605 );
606 let verifier = DkimVerifier::new(resolver);
607 let headers: Vec<(&str, &str)> = vec![
609 ("From", " user@example.com"),
610 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
611 ];
612 let results = verifier.verify_message(&headers, b"body").await;
613 match &results[0] {
614 DkimResult::PermFail { kind, .. } => assert_eq!(*kind, PermFailKind::HashNotPermitted),
615 _ => panic!("expected PermFail HashNotPermitted"),
616 }
617 }
618
619 #[tokio::test]
621 async fn key_s_rejects_email() {
622 let mut resolver = MockResolver::new();
623 let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
624 setup_mock_key(
625 &mut resolver,
626 "sel1",
627 "example.com",
628 &format!("v=DKIM1; s=other; k=rsa; p={}", p),
629 );
630 let verifier = DkimVerifier::new(resolver);
631 let headers: Vec<(&str, &str)> = vec![
632 ("From", " user@example.com"),
633 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
634 ];
635 let results = verifier.verify_message(&headers, b"body").await;
636 match &results[0] {
637 DkimResult::PermFail { kind, .. } => {
638 assert_eq!(*kind, PermFailKind::ServiceTypeMismatch)
639 }
640 _ => panic!("expected PermFail ServiceTypeMismatch"),
641 }
642 }
643
644 #[tokio::test]
646 async fn key_strict_mode_violation() {
647 let mut resolver = MockResolver::new();
648 let p = base64::engine::general_purpose::STANDARD.encode(vec![0x30u8; 162]);
649 setup_mock_key(
650 &mut resolver,
651 "sel1",
652 "example.com",
653 &format!("v=DKIM1; t=s; k=rsa; p={}", p),
654 );
655 let verifier = DkimVerifier::new(resolver);
656 let headers: Vec<(&str, &str)> = vec![
658 ("From", " user@sub.example.com"),
659 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; i=user@sub.example.com"),
660 ];
661 let results = verifier.verify_message(&headers, b"body").await;
662 match &results[0] {
663 DkimResult::PermFail { kind, .. } => {
664 assert_eq!(*kind, PermFailKind::StrictModeViolation)
665 }
666 _ => panic!("expected PermFail StrictModeViolation"),
667 }
668 }
669
670 #[tokio::test]
672 async fn algorithm_key_type_mismatch() {
673 let mut resolver = MockResolver::new();
674 let p = base64::engine::general_purpose::STANDARD.encode(vec![0xABu8; 32]);
675 setup_mock_key(
677 &mut resolver,
678 "sel1",
679 "example.com",
680 &format!("v=DKIM1; k=ed25519; p={}", p),
681 );
682 let verifier = DkimVerifier::new(resolver);
683 let headers: Vec<(&str, &str)> = vec![
684 ("From", " user@example.com"),
685 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1"),
686 ];
687 let results = verifier.verify_message(&headers, b"body").await;
688 match &results[0] {
689 DkimResult::PermFail { kind, .. } => {
690 assert_eq!(*kind, PermFailKind::AlgorithmMismatch)
691 }
692 _ => panic!("expected PermFail AlgorithmMismatch"),
693 }
694 }
695
696 #[tokio::test]
700 async fn expired_signature() {
701 let resolver = MockResolver::new();
702 let verifier = DkimVerifier::new(resolver);
703 let headers: Vec<(&str, &str)> = vec![
705 ("From", " user@example.com"),
706 ("DKIM-Signature", " v=1; a=rsa-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; x=1000000"),
707 ];
708 let results = verifier.verify_message(&headers, b"body").await;
709 match &results[0] {
710 DkimResult::PermFail { kind, .. } => {
711 assert_eq!(*kind, PermFailKind::ExpiredSignature)
712 }
713 _ => panic!("expected PermFail ExpiredSignature"),
714 }
715 }
716
717 #[tokio::test]
719 async fn expiration_before_timestamp() {
720 let resolver = MockResolver::new();
721 let verifier = DkimVerifier::new(resolver);
722 let headers: Vec<(&str, &str)> = vec![
724 ("From", " user@example.com"),
725 ("DKIM-Signature", " v=1; a=ed25519-sha256; b=dGVzdA==; bh=dGVzdA==; d=example.com; h=from; s=sel1; t=2000000; x=1000000"),
726 ];
727 let results = verifier.verify_message(&headers, b"body").await;
728 match &results[0] {
729 DkimResult::PermFail { kind, .. } => {
730 assert_eq!(*kind, PermFailKind::MalformedSignature)
731 }
732 other => panic!("expected PermFail MalformedSignature, got {:?}", other),
733 }
734 }
735
736 #[tokio::test]
739 async fn ed25519_pass_ground_truth() {
740 let (pkcs8, public_key) = gen_ed25519_keypair();
741 let body = b"Hello DKIM\r\n";
742 let domain = "example.com";
743 let selector = "ed";
744
745 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
747
748 let sig_header_template = format!(
750 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
751 bh, domain, selector
752 );
753
754 let header_input = compute_header_input_manual(
756 &[("From", " user@example.com")],
757 &["from"],
758 &sig_header_template,
759 CanonicalizationMethod::Relaxed,
760 );
761
762 let signature = ed25519_sign(&pkcs8, &header_input);
764 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
765
766 let final_sig_header = format!(
768 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
769 sig_b64, bh, domain, selector
770 );
771
772 let mut resolver = MockResolver::new();
774 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
775 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
776
777 let verifier = DkimVerifier::new(resolver);
778 let headers: Vec<(&str, &str)> = vec![
779 ("From", " user@example.com"),
780 ("DKIM-Signature", &final_sig_header),
781 ];
782
783 let results = verifier.verify_message(&headers, body).await;
784 match &results[0] {
785 DkimResult::Pass { domain: d, selector: s, testing } => {
786 assert_eq!(d, domain);
787 assert_eq!(s, selector);
788 assert!(!testing);
789 }
790 other => panic!("expected Pass, got {:?}", other),
791 }
792 }
793
794 #[tokio::test]
797 async fn ed25519_tampered_body() {
798 let (pkcs8, public_key) = gen_ed25519_keypair();
799 let body = b"Hello DKIM\r\n";
800 let domain = "example.com";
801 let selector = "ed";
802
803 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
804 let sig_header_template = format!(
805 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
806 bh, domain, selector
807 );
808
809 let header_input = compute_header_input_manual(
810 &[("From", " user@example.com")],
811 &["from"],
812 &sig_header_template,
813 CanonicalizationMethod::Relaxed,
814 );
815
816 let signature = ed25519_sign(&pkcs8, &header_input);
817 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
818
819 let final_sig_header = format!(
820 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
821 sig_b64, bh, domain, selector
822 );
823
824 let mut resolver = MockResolver::new();
825 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
826 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
827
828 let verifier = DkimVerifier::new(resolver);
829 let headers: Vec<(&str, &str)> = vec![
830 ("From", " user@example.com"),
831 ("DKIM-Signature", &final_sig_header),
832 ];
833
834 let results = verifier.verify_message(&headers, b"TAMPERED BODY\r\n").await;
836 match &results[0] {
837 DkimResult::Fail { kind, .. } => assert_eq!(*kind, FailureKind::BodyHashMismatch),
838 other => panic!("expected Fail BodyHashMismatch, got {:?}", other),
839 }
840 }
841
842 #[tokio::test]
845 async fn ed25519_tampered_header() {
846 let (pkcs8, public_key) = gen_ed25519_keypair();
847 let body = b"Hello DKIM\r\n";
848 let domain = "example.com";
849 let selector = "ed";
850
851 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
852 let sig_header_template = format!(
853 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:subject; s={}",
854 bh, domain, selector
855 );
856
857 let header_input = compute_header_input_manual(
858 &[("From", " user@example.com"), ("Subject", " original")],
859 &["from", "subject"],
860 &sig_header_template,
861 CanonicalizationMethod::Relaxed,
862 );
863
864 let signature = ed25519_sign(&pkcs8, &header_input);
865 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
866
867 let final_sig_header = format!(
868 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:subject; s={}",
869 sig_b64, bh, domain, selector
870 );
871
872 let mut resolver = MockResolver::new();
873 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
874 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
875
876 let verifier = DkimVerifier::new(resolver);
877 let headers: Vec<(&str, &str)> = vec![
879 ("From", " user@example.com"),
880 ("Subject", " TAMPERED"),
881 ("DKIM-Signature", &final_sig_header),
882 ];
883
884 let results = verifier.verify_message(&headers, body).await;
885 match &results[0] {
886 DkimResult::Fail { kind, .. } => {
887 assert_eq!(*kind, FailureKind::SignatureVerificationFailed)
888 }
889 other => panic!("expected Fail SigVerificationFailed, got {:?}", other),
890 }
891 }
892
893 #[tokio::test]
896 async fn ed25519_simple_simple_e2e() {
897 let (pkcs8, public_key) = gen_ed25519_keypair();
898 let body = b"Simple body content\r\n";
899 let domain = "example.com";
900 let selector = "ed";
901
902 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Simple);
903 let sig_header_template = format!(
904 " v=1; a=ed25519-sha256; b=; bh={}; c=simple/simple; d={}; h=from; s={}",
905 bh, domain, selector
906 );
907
908 let header_input = compute_header_input_manual(
909 &[("From", " user@example.com")],
910 &["from"],
911 &sig_header_template,
912 CanonicalizationMethod::Simple,
913 );
914
915 let signature = ed25519_sign(&pkcs8, &header_input);
916 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
917
918 let final_sig_header = format!(
919 " v=1; a=ed25519-sha256; b={}; bh={}; c=simple/simple; d={}; h=from; s={}",
920 sig_b64, bh, domain, selector
921 );
922
923 let mut resolver = MockResolver::new();
924 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
925 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
926
927 let verifier = DkimVerifier::new(resolver);
928 let headers: Vec<(&str, &str)> = vec![
929 ("From", " user@example.com"),
930 ("DKIM-Signature", &final_sig_header),
931 ];
932
933 let results = verifier.verify_message(&headers, body).await;
934 match &results[0] {
935 DkimResult::Pass { .. } => {}
936 other => panic!("expected Pass, got {:?}", other),
937 }
938 }
939
940 #[tokio::test]
943 async fn ed25519_relaxed_relaxed_e2e() {
944 let (pkcs8, public_key) = gen_ed25519_keypair();
945 let body = b"Relaxed body content \r\n";
946 let domain = "example.com";
947 let selector = "ed";
948
949 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
950 let sig_header_template = format!(
951 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
952 bh, domain, selector
953 );
954
955 let header_input = compute_header_input_manual(
956 &[("From", " user@example.com")],
957 &["from"],
958 &sig_header_template,
959 CanonicalizationMethod::Relaxed,
960 );
961
962 let signature = ed25519_sign(&pkcs8, &header_input);
963 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
964
965 let final_sig_header = format!(
966 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
967 sig_b64, bh, domain, selector
968 );
969
970 let mut resolver = MockResolver::new();
971 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
972 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
973
974 let verifier = DkimVerifier::new(resolver);
975 let headers: Vec<(&str, &str)> = vec![
976 ("From", " user@example.com"),
977 ("DKIM-Signature", &final_sig_header),
978 ];
979
980 let results = verifier.verify_message(&headers, body).await;
981 match &results[0] {
982 DkimResult::Pass { .. } => {}
983 other => panic!("expected Pass, got {:?}", other),
984 }
985 }
986
987 #[tokio::test]
990 async fn ed25519_over_signed_verify() {
991 let (pkcs8, public_key) = gen_ed25519_keypair();
992 let body = b"body\r\n";
993 let domain = "example.com";
994 let selector = "ed";
995
996 let bh = compute_body_hash(body, Algorithm::Ed25519Sha256, CanonicalizationMethod::Relaxed);
997 let sig_header_template = format!(
999 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:from; s={}",
1000 bh, domain, selector
1001 );
1002
1003 let header_input = compute_header_input_manual(
1004 &[("From", " user@example.com")],
1005 &["from", "from"],
1006 &sig_header_template,
1007 CanonicalizationMethod::Relaxed,
1008 );
1009
1010 let signature = ed25519_sign(&pkcs8, &header_input);
1011 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&signature);
1012
1013 let final_sig_header = format!(
1014 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:from; s={}",
1015 sig_b64, bh, domain, selector
1016 );
1017
1018 let mut resolver = MockResolver::new();
1019 let p = base64::engine::general_purpose::STANDARD.encode(&public_key);
1020 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p));
1021
1022 let verifier = DkimVerifier::new(resolver);
1023 let headers: Vec<(&str, &str)> = vec![
1024 ("From", " user@example.com"),
1025 ("DKIM-Signature", &final_sig_header),
1026 ];
1027
1028 let results = verifier.verify_message(&headers, body).await;
1029 match &results[0] {
1030 DkimResult::Pass { .. } => {}
1031 other => panic!("expected Pass with over-signing, got {:?}", other),
1032 }
1033 }
1034
1035 #[tokio::test]
1041 async fn ground_truth_ed25519_manual_ring_primitives() {
1042 let (pkcs8, public_key) = gen_ed25519_keypair();
1045 let body = b"Ground truth test body\r\n";
1046 let domain = "gt.example.com";
1047 let selector = "gtsel";
1048
1049 let normalized_body = normalize_line_endings(body);
1051 let canon_body = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_body);
1052 let body_hash = ring::digest::digest(&ring::digest::SHA256, &canon_body);
1053 let bh_b64 = base64::engine::general_purpose::STANDARD.encode(body_hash.as_ref());
1054
1055 let sig_template = format!(
1057 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from:to; s={}",
1058 bh_b64, domain, selector
1059 );
1060
1061 let msg_headers = vec![
1063 ("From", " sender@gt.example.com"),
1064 ("To", " receiver@gt.example.com"),
1065 ];
1066 let header_input = compute_header_input_manual(
1067 &msg_headers,
1068 &["from", "to"],
1069 &sig_template,
1070 CanonicalizationMethod::Relaxed,
1071 );
1072
1073 let key_pair = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
1075 let sig_bytes = key_pair.sign(&header_input);
1076 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig_bytes.as_ref());
1077
1078 let final_sig = format!(
1080 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from:to; s={}",
1081 sig_b64, bh_b64, domain, selector
1082 );
1083
1084 let mut resolver = MockResolver::new();
1086 let p_b64 = base64::engine::general_purpose::STANDARD.encode(&public_key);
1087 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p_b64));
1088
1089 let verifier = DkimVerifier::new(resolver);
1090 let full_headers: Vec<(&str, &str)> = vec![
1091 ("From", " sender@gt.example.com"),
1092 ("To", " receiver@gt.example.com"),
1093 ("DKIM-Signature", &final_sig),
1094 ];
1095
1096 let results = verifier.verify_message(&full_headers, body).await;
1097 match &results[0] {
1098 DkimResult::Pass { domain: d, .. } => assert_eq!(d, domain),
1099 other => panic!("Ground-truth test failed: {:?}", other),
1100 }
1101 }
1102
1103 #[tokio::test]
1105 async fn ground_truth_ed25519_tampered() {
1106 let (pkcs8, public_key) = gen_ed25519_keypair();
1107 let body = b"Original body\r\n";
1108 let domain = "gt.example.com";
1109 let selector = "gtsel";
1110
1111 let normalized_body = normalize_line_endings(body);
1112 let canon_body = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_body);
1113 let body_hash = ring::digest::digest(&ring::digest::SHA256, &canon_body);
1114 let bh_b64 = base64::engine::general_purpose::STANDARD.encode(body_hash.as_ref());
1115
1116 let sig_template = format!(
1117 " v=1; a=ed25519-sha256; b=; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
1118 bh_b64, domain, selector
1119 );
1120
1121 let header_input = compute_header_input_manual(
1122 &[("From", " sender@gt.example.com")],
1123 &["from"],
1124 &sig_template,
1125 CanonicalizationMethod::Relaxed,
1126 );
1127
1128 let key_pair = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
1129 let sig_bytes = key_pair.sign(&header_input);
1130 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig_bytes.as_ref());
1131
1132 let final_sig = format!(
1133 " v=1; a=ed25519-sha256; b={}; bh={}; c=relaxed/relaxed; d={}; h=from; s={}",
1134 sig_b64, bh_b64, domain, selector
1135 );
1136
1137 let mut resolver = MockResolver::new();
1138 let p_b64 = base64::engine::general_purpose::STANDARD.encode(&public_key);
1139 setup_mock_key(&mut resolver, selector, domain, &format!("k=ed25519; p={}", p_b64));
1140
1141 let verifier = DkimVerifier::new(resolver);
1142 let full_headers: Vec<(&str, &str)> = vec![
1143 ("From", " sender@gt.example.com"),
1144 ("DKIM-Signature", &final_sig),
1145 ];
1146
1147 let results = verifier
1149 .verify_message(&full_headers, b"Tampered body\r\n")
1150 .await;
1151 match &results[0] {
1152 DkimResult::Fail { kind, .. } => assert_eq!(*kind, FailureKind::BodyHashMismatch),
1153 other => panic!("expected BodyHashMismatch, got {:?}", other),
1154 }
1155 }
1156
1157 const RSA_FIXTURE_SPKI_B64: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0zzOtswuqB21FtQ+W5cgh7DiaJ+4TMQBmSm3V6gKYSq/WPbg0vaSdlru6PBbdocwBrn4di2bNpdy5Co1ujLtogyg6+f4A9K36CygLWqOhygt6A/Rl94daXwew1S/7oSksuAGnSg2+XMuU2An+IHSYnx/qAiGnnzkhGgsTnUnJxZ2mitvKemPjTDIB2dz1hJAmnJS0ffUADnSXgS55f8aXAdRRDQwYlTwBRLrdpcQzVRKU+L/hm4EzePVkvXUgeuBKqhIosNHl28fuN1nac3zuosWorJQ7Ox2MKKdVB5FkT85mZp/i7L0+/JMVXJeNHnlFe3OqFUEKmYpgL37oTGCdQIDAQAB";
1169
1170 #[tokio::test]
1171 async fn rsa_sha256_precomputed_fixture_pass() {
1172 let body = b"Test body for RSA verification\r\n";
1173 let sig_value = concat!(
1174 " v=1; a=rsa-sha256; b=zm5IJ/e9WakQhZ+pmKQafoSc2iZE2xGfYA7sbWF+O8vhES09D7HyUo",
1175 "sQVnG4fm6mHOc6pHLtTaQDe/4r0tOjjI7peVO8BCi3KSQtKZIORJ8wrs3PQLpZtZdK/zlfIZywW0",
1176 "n5DMxbHU+uqjkR4y191xYg/fWZaC14d/4V5RvzKb8ZV7qYzpi5EWDlXTCbJTryuJydjRVYIa1F+6",
1177 "cI3ROJn8U9GcyGcJJQo5HrrWYKAiPGhR3sXjKbBEOah7CH5XQv22j4Q3q2LhNjtTnXrS77rvw8lu",
1178 "b+H0e8vEB4Ps4Y9y81QPGqs9Xse2MakBVER44/1M4XvlpS+5bD4bUZfYG5cQ==; bh=A82pV6ef4",
1179 "eO/+6HFHShh58CZ7NYOh4gNm0JUpCe9AJU=; c=relaxed/relaxed; d=example.com; h=fro",
1180 "m:to:subject; s=rsa2048",
1181 );
1182
1183 let mut resolver = MockResolver::new();
1184 setup_mock_key(
1185 &mut resolver,
1186 "rsa2048",
1187 "example.com",
1188 &format!("v=DKIM1; k=rsa; p={}", RSA_FIXTURE_SPKI_B64),
1189 );
1190
1191 let verifier = DkimVerifier::new(resolver);
1192 let headers: Vec<(&str, &str)> = vec![
1193 ("From", " user@example.com"),
1194 ("To", " recipient@example.com"),
1195 ("Subject", " RSA test"),
1196 ("DKIM-Signature", sig_value),
1197 ];
1198
1199 let results = verifier.verify_message(&headers, body).await;
1200 match &results[0] {
1201 DkimResult::Pass { domain, selector, .. } => {
1202 assert_eq!(domain, "example.com");
1203 assert_eq!(selector, "rsa2048");
1204 }
1205 other => panic!("RSA-SHA256 fixture: expected Pass, got {:?}", other),
1206 }
1207 }
1208
1209 #[tokio::test]
1213 async fn rsa_sha1_precomputed_fixture_pass() {
1214 let body = b"Test body for RSA verification\r\n";
1215 let sig_value = concat!(
1216 " v=1; a=rsa-sha1; b=Y9CjLLQ3d8kw7z7FnjDF7YDbD5jV8F4nmNN2IP7HcIIJFMmEdvE2+mMH",
1217 "OulTI26Kp7x+r0aubcmOAvOUh1eFX2t7359bnVL9n1MEKIcxdZO3fIU5LhXBAfrkILe/caA5hQgU",
1218 "94HdPiOyGUNIQdGIG4ECZ6zdcW1K4TVYQmGawJzwKyKo1m4MqT99bJot5MUEmK/7jX9aROrDtwok",
1219 "qtFAysXpmqWj3lOg+IJSmiKzD0DvbvU1G/LE4T95zjnot+rBtC0/jJ/ooq0ZBBOvC5KHQ0pwDxCC",
1220 "ENR18UkcyZG/6FRLFGGzReJPQViJ4XqBNpDOovEXj3v4q9tdBmNt5zNKzQ==; bh=wIO7ahU/Pub",
1221 "98XWknH1rIcruxRc=; c=relaxed/relaxed; d=example.com; h=from:to:subject; s=rs",
1222 "a2048",
1223 );
1224
1225 let mut resolver = MockResolver::new();
1226 setup_mock_key(
1227 &mut resolver,
1228 "rsa2048",
1229 "example.com",
1230 &format!("v=DKIM1; k=rsa; p={}", RSA_FIXTURE_SPKI_B64),
1231 );
1232
1233 let verifier = DkimVerifier::new(resolver);
1234 let headers: Vec<(&str, &str)> = vec![
1235 ("From", " user@example.com"),
1236 ("To", " recipient@example.com"),
1237 ("Subject", " RSA test"),
1238 ("DKIM-Signature", sig_value),
1239 ];
1240
1241 let results = verifier.verify_message(&headers, body).await;
1242 match &results[0] {
1243 DkimResult::Pass { domain, selector, .. } => {
1244 assert_eq!(domain, "example.com");
1245 assert_eq!(selector, "rsa2048");
1246 }
1247 other => panic!("RSA-SHA1 fixture: expected Pass, got {:?}", other),
1248 }
1249 }
1250
1251 #[test]
1259 fn strip_spki_passthrough_pkcs1() {
1260 let data = vec![0x30, 0x82, 0x00]; assert_eq!(strip_spki_wrapper(&data), data.as_slice());
1263 }
1264
1265 #[test]
1266 fn strip_spki_too_short() {
1267 let data = vec![0x30; 10];
1268 assert_eq!(strip_spki_wrapper(&data), data.as_slice());
1269 }
1270
1271 #[test]
1272 fn strip_spki_real_rsa_2048_key() {
1273 let spki_der = base64::engine::general_purpose::STANDARD
1275 .decode(RSA_FIXTURE_SPKI_B64)
1276 .unwrap();
1277 assert_eq!(spki_der.len(), 294); let pkcs1 = strip_spki_wrapper(&spki_der);
1280 assert!(pkcs1.len() < spki_der.len());
1282 assert_eq!(pkcs1[0], 0x30);
1284 assert!(pkcs1.len() > 250); }
1287}