1use crate::common::dns::{DnsError, DnsResolver};
2use crate::dkim::canon::{
3 apply_body_length_limit, canonicalize_body, canonicalize_header, normalize_line_endings,
4 select_headers, strip_b_tag_value,
5};
6use crate::dkim::key::DkimPublicKey;
7use crate::dkim::types::CanonicalizationMethod;
8use crate::dkim::verify::{compute_hash, verify_signature};
9
10use subtle::ConstantTimeEq;
11
12use super::parser::collect_arc_sets;
13use super::types::{
14 ArcMessageSignature, ArcResult, ArcSeal, ArcSet, ArcValidationResult,
15 ChainValidationStatus,
16};
17
18pub struct ArcVerifier<R: DnsResolver> {
20 resolver: R,
21}
22
23impl<R: DnsResolver> ArcVerifier<R> {
24 pub fn new(resolver: R) -> Self {
25 Self { resolver }
26 }
27
28 pub async fn validate_chain(
30 &self,
31 headers: &[(&str, &str)],
32 body: &[u8],
33 ) -> ArcValidationResult {
34 let sets = match collect_arc_sets(headers) {
36 Ok(s) => s,
37 Err(e) => {
38 return ArcValidationResult {
39 status: ArcResult::Fail {
40 reason: e.detail,
41 },
42 oldest_pass: Option::None,
43 };
44 }
45 };
46
47 if sets.is_empty() {
49 return ArcValidationResult {
50 status: ArcResult::None,
51 oldest_pass: Option::None,
52 };
53 }
54
55 let n = sets.len();
56
57 if n > 50 {
59 return ArcValidationResult {
60 status: ArcResult::Fail {
61 reason: format!("too many ARC sets: {}", n),
62 },
63 oldest_pass: Option::None,
64 };
65 }
66
67 let latest = &sets[n - 1];
69 if latest.seal.cv == ChainValidationStatus::Fail {
70 return ArcValidationResult {
71 status: ArcResult::Fail {
72 reason: format!("AS({}) has cv=fail", n),
73 },
74 oldest_pass: Option::None,
75 };
76 }
77
78 if let Err(reason) = validate_structure(&sets) {
80 return ArcValidationResult {
81 status: ArcResult::Fail { reason },
82 oldest_pass: Option::None,
83 };
84 }
85
86 if let Err(reason) = self.validate_ams(&sets[n - 1].ams, headers, body).await {
88 return ArcValidationResult {
89 status: ArcResult::Fail {
90 reason: format!("AMS({}) validation failed: {}", n, reason),
91 },
92 oldest_pass: Option::None,
93 };
94 }
95
96 let mut oldest_pass: u32 = 0;
99 for i in (0..n - 1).rev() {
100 if let Err(_) = self.validate_ams(&sets[i].ams, headers, body).await {
101 oldest_pass = (i + 2) as u32; break;
103 }
104 }
105
106 for i in (0..n).rev() {
108 if let Err(reason) = self.validate_seal(&sets[i].seal, &sets, headers).await {
109 return ArcValidationResult {
110 status: ArcResult::Fail {
111 reason: format!("AS({}) validation failed: {}", i + 1, reason),
112 },
113 oldest_pass: Option::None,
114 };
115 }
116 }
117
118 ArcValidationResult {
120 status: ArcResult::Pass,
121 oldest_pass: Some(oldest_pass),
122 }
123 }
124
125 async fn validate_ams(
127 &self,
128 ams: &ArcMessageSignature,
129 headers: &[(&str, &str)],
130 body: &[u8],
131 ) -> Result<(), String> {
132 let normalized = normalize_line_endings(body);
134 let canonicalized = canonicalize_body(ams.body_canonicalization, &normalized);
135 let limited = apply_body_length_limit(&canonicalized, ams.body_length);
136 let computed_body_hash = compute_hash(ams.algorithm, limited);
137
138 if !bool::from(computed_body_hash.ct_eq(&ams.body_hash)) {
139 return Err("body hash mismatch".to_string());
140 }
141
142 let non_arc_headers: Vec<(&str, &str)> = headers
145 .iter()
146 .filter(|(name, _)| {
147 let lower = name.to_ascii_lowercase();
148 lower != "arc-authentication-results"
149 && lower != "arc-message-signature"
150 && lower != "arc-seal"
151 })
152 .copied()
153 .collect();
154
155 let selected = select_headers(
156 ams.header_canonicalization,
157 &ams.signed_headers,
158 &non_arc_headers,
159 );
160
161 let mut hash_input = Vec::new();
162 for header_line in &selected {
163 hash_input.extend_from_slice(header_line.as_bytes());
164 }
165
166 let stripped = strip_b_tag_value(&ams.raw_header);
168 let canon_ams = canonicalize_header(
169 ams.header_canonicalization,
170 "arc-message-signature",
171 &stripped,
172 );
173 hash_input.extend_from_slice(canon_ams.as_bytes());
174
175 let key = self.lookup_key(&ams.selector, &ams.domain).await?;
177
178 verify_signature(&ams.algorithm, &key, &hash_input, &ams.signature)
180 }
181
182 async fn validate_seal(
184 &self,
185 seal: &ArcSeal,
186 sets: &[ArcSet],
187 _headers: &[(&str, &str)],
188 ) -> Result<(), String> {
189 let instance = seal.instance as usize;
190
191 let mut hash_input = Vec::new();
193
194 for set_idx in 0..instance {
195 let set = &sets[set_idx];
196
197 let aar_canon = canonicalize_header(
199 CanonicalizationMethod::Relaxed,
200 "arc-authentication-results",
201 &set.aar.raw_header,
202 );
203 hash_input.extend_from_slice(aar_canon.as_bytes());
204
205 let ams_canon = canonicalize_header(
207 CanonicalizationMethod::Relaxed,
208 "arc-message-signature",
209 &set.ams.raw_header,
210 );
211 hash_input.extend_from_slice(ams_canon.as_bytes());
212
213 if set_idx == instance - 1 {
215 let stripped = strip_b_tag_value(&set.seal.raw_header);
217 let canon_seal = canonicalize_header(
218 CanonicalizationMethod::Relaxed,
219 "arc-seal",
220 &stripped,
221 );
222 let seal_bytes = canon_seal.as_bytes();
224 if seal_bytes.ends_with(b"\r\n") {
225 hash_input.extend_from_slice(&seal_bytes[..seal_bytes.len() - 2]);
226 } else {
227 hash_input.extend_from_slice(seal_bytes);
228 }
229 } else {
230 let seal_canon = canonicalize_header(
231 CanonicalizationMethod::Relaxed,
232 "arc-seal",
233 &set.seal.raw_header,
234 );
235 hash_input.extend_from_slice(seal_canon.as_bytes());
236 }
237 }
238
239 let key = self.lookup_key(&seal.selector, &seal.domain).await?;
241
242 verify_signature(&seal.algorithm, &key, &hash_input, &seal.signature)
244 }
245
246 async fn lookup_key(&self, selector: &str, domain: &str) -> Result<DkimPublicKey, String> {
248 let query = format!("{}._domainkey.{}", selector, domain);
249 let txt_records = match self.resolver.query_txt(&query).await {
250 Ok(records) => records,
251 Err(DnsError::NxDomain) | Err(DnsError::NoRecords) => {
252 return Err(format!("no DNS key record at {}", query));
253 }
254 Err(DnsError::TempFail) => {
255 return Err(format!("DNS temp failure for {}", query));
256 }
257 };
258
259 let concatenated = txt_records.join("");
260 DkimPublicKey::parse(&concatenated).map_err(|e| e.detail)
261 }
262}
263
264fn validate_structure(sets: &[ArcSet]) -> Result<(), String> {
266 for (idx, set) in sets.iter().enumerate() {
267 let expected_instance = (idx + 1) as u32;
268 if set.instance != expected_instance {
269 return Err(format!(
270 "expected instance {}, got {}",
271 expected_instance, set.instance
272 ));
273 }
274
275 if set.instance == 1 && set.seal.cv != ChainValidationStatus::None {
277 return Err(format!(
278 "instance 1 must have cv=none, got {:?}",
279 set.seal.cv
280 ));
281 }
282
283 if set.instance > 1 && set.seal.cv != ChainValidationStatus::Pass {
285 return Err(format!(
286 "instance {} must have cv=pass, got {:?}",
287 set.instance, set.seal.cv
288 ));
289 }
290 }
291 Ok(())
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::arc::types::ArcAuthenticationResults;
298 use crate::common::dns::mock::MockResolver;
299 use crate::dkim::types::Algorithm;
300 use base64::Engine;
301 use ring::rand::SystemRandom;
302 use ring::signature::{Ed25519KeyPair, KeyPair};
303
304 fn gen_ed25519_keypair() -> (Vec<u8>, Vec<u8>) {
305 let rng = SystemRandom::new();
306 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
307 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
308 let public_key = key_pair.public_key().as_ref().to_vec();
309 (pkcs8.as_ref().to_vec(), public_key)
310 }
311
312 fn ed25519_sign(pkcs8: &[u8], data: &[u8]) -> Vec<u8> {
313 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8).unwrap();
314 key_pair.sign(data).as_ref().to_vec()
315 }
316
317 fn b64(data: &[u8]) -> String {
318 base64::engine::general_purpose::STANDARD.encode(data)
319 }
320
321 fn make_dns_key_record(public_key: &[u8]) -> String {
322 format!("v=DKIM1; k=ed25519; p={}", b64(public_key))
323 }
324
325 fn build_single_arc_set(
328 ) -> (Vec<(String, String)>, MockResolver, Vec<u8>, Vec<u8>) {
329 let (pkcs8, pub_key) = gen_ed25519_keypair();
330
331 let body = b"Hello, world!\r\n";
332 let message_headers = vec![
333 ("From".to_string(), "sender@example.com".to_string()),
334 ("To".to_string(), "recipient@example.com".to_string()),
335 ("Subject".to_string(), "test".to_string()),
336 ];
337
338 let normalized = normalize_line_endings(body);
340 let canonicalized = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized);
341 let body_hash = compute_hash(Algorithm::Ed25519Sha256, &canonicalized);
342
343 let ams_raw_no_b = format!(
345 "i=1; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:to:subject; bh={}; b=",
346 b64(&body_hash)
347 );
348
349 let non_arc_headers: Vec<(&str, &str)> = message_headers
351 .iter()
352 .map(|(n, v)| (n.as_str(), v.as_str()))
353 .collect();
354
355 let selected = select_headers(
356 CanonicalizationMethod::Relaxed,
357 &["from".to_string(), "to".to_string(), "subject".to_string()],
358 &non_arc_headers,
359 );
360 let mut ams_hash_input = Vec::new();
361 for h in &selected {
362 ams_hash_input.extend_from_slice(h.as_bytes());
363 }
364 let canon_ams = canonicalize_header(
365 CanonicalizationMethod::Relaxed,
366 "arc-message-signature",
367 &ams_raw_no_b,
368 );
369 ams_hash_input.extend_from_slice(canon_ams.as_bytes());
370
371 let ams_sig = ed25519_sign(&pkcs8, &ams_hash_input);
372 let ams_raw = format!(
373 "i=1; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:to:subject; bh={}; b={}",
374 b64(&body_hash),
375 b64(&ams_sig),
376 );
377
378 let aar_raw = "i=1; spf=pass smtp.mailfrom=example.com".to_string();
380
381 let seal_raw_no_b =
383 "i=1; cv=none; a=ed25519-sha256; d=sealer.com; s=arc; b=".to_string();
384
385 let mut seal_hash_input = Vec::new();
387 let canon_aar = canonicalize_header(
388 CanonicalizationMethod::Relaxed,
389 "arc-authentication-results",
390 &aar_raw,
391 );
392 seal_hash_input.extend_from_slice(canon_aar.as_bytes());
393 let canon_ams_full = canonicalize_header(
394 CanonicalizationMethod::Relaxed,
395 "arc-message-signature",
396 &ams_raw,
397 );
398 seal_hash_input.extend_from_slice(canon_ams_full.as_bytes());
399 let canon_seal = canonicalize_header(
400 CanonicalizationMethod::Relaxed,
401 "arc-seal",
402 &seal_raw_no_b,
403 );
404 let seal_bytes = canon_seal.as_bytes();
406 if seal_bytes.ends_with(b"\r\n") {
407 seal_hash_input.extend_from_slice(&seal_bytes[..seal_bytes.len() - 2]);
408 } else {
409 seal_hash_input.extend_from_slice(seal_bytes);
410 }
411
412 let seal_sig = ed25519_sign(&pkcs8, &seal_hash_input);
413 let seal_raw = format!(
414 "i=1; cv=none; a=ed25519-sha256; d=sealer.com; s=arc; b={}",
415 b64(&seal_sig),
416 );
417
418 let mut all_headers = vec![
420 ("ARC-Seal".to_string(), seal_raw),
421 ("ARC-Message-Signature".to_string(), ams_raw),
422 ("ARC-Authentication-Results".to_string(), aar_raw),
423 ];
424 all_headers.extend(message_headers);
425
426 let mut resolver = MockResolver::new();
428 resolver.add_txt(
429 "arc._domainkey.sealer.com",
430 vec![make_dns_key_record(&pub_key)],
431 );
432
433 (all_headers, resolver, body.to_vec(), pkcs8)
434 }
435
436 #[tokio::test]
439 async fn no_arc_headers_none() {
440 let resolver = MockResolver::new();
441 let verifier = ArcVerifier::new(resolver);
442 let headers = vec![("From", "test@example.com")];
443 let result = verifier.validate_chain(&headers, b"body").await;
444 assert_eq!(result.status, ArcResult::None);
445 }
446
447 #[tokio::test]
450 async fn latest_cv_fail_immediately() {
451 let headers = vec![
452 ("ARC-Authentication-Results", "i=1; spf=pass"),
453 (
454 "ARC-Message-Signature",
455 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
456 ),
457 (
458 "ARC-Seal",
459 "i=1; cv=fail; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
460 ),
461 ];
462 let resolver = MockResolver::new();
463 let verifier = ArcVerifier::new(resolver);
464 let result = verifier.validate_chain(&headers, b"body").await;
465 assert!(matches!(result.status, ArcResult::Fail { .. }));
466 }
467
468 #[tokio::test]
471 async fn instance_1_cv_pass_fails() {
472 let headers = vec![
473 ("ARC-Authentication-Results", "i=1; spf=pass"),
474 (
475 "ARC-Message-Signature",
476 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
477 ),
478 (
479 "ARC-Seal",
480 "i=1; cv=pass; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
481 ),
482 ];
483 let resolver = MockResolver::new();
484 let verifier = ArcVerifier::new(resolver);
485 let result = verifier.validate_chain(&headers, b"body").await;
486 assert!(matches!(result.status, ArcResult::Fail { .. }));
487 }
488
489 #[tokio::test]
492 async fn instance_2_cv_none_fails() {
493 let headers = vec![
495 ("ARC-Authentication-Results", "i=1; spf=pass"),
496 (
497 "ARC-Message-Signature",
498 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
499 ),
500 (
501 "ARC-Seal",
502 "i=1; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
503 ),
504 ("ARC-Authentication-Results", "i=2; dkim=pass"),
505 (
506 "ARC-Message-Signature",
507 "i=2; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
508 ),
509 (
510 "ARC-Seal",
511 "i=2; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
512 ),
513 ];
514 let resolver = MockResolver::new();
515 let verifier = ArcVerifier::new(resolver);
516 let result = verifier.validate_chain(&headers, b"body").await;
517 assert!(matches!(result.status, ArcResult::Fail { .. }));
518 }
519
520 #[tokio::test]
523 async fn single_arc_set_pass() {
524 let (headers_owned, resolver, body, _) = build_single_arc_set();
525 let headers: Vec<(&str, &str)> = headers_owned
526 .iter()
527 .map(|(n, v)| (n.as_str(), v.as_str()))
528 .collect();
529 let verifier = ArcVerifier::new(resolver);
530 let result = verifier.validate_chain(&headers, &body).await;
531 assert_eq!(result.status, ArcResult::Pass);
532 assert_eq!(result.oldest_pass, Some(0)); }
534
535 #[tokio::test]
538 async fn gap_in_instances_fails() {
539 let headers = vec![
541 ("ARC-Authentication-Results", "i=1; spf=pass"),
542 (
543 "ARC-Message-Signature",
544 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
545 ),
546 (
547 "ARC-Seal",
548 "i=1; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
549 ),
550 ("ARC-Authentication-Results", "i=3; spf=pass"),
551 (
552 "ARC-Message-Signature",
553 "i=3; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
554 ),
555 (
556 "ARC-Seal",
557 "i=3; cv=pass; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
558 ),
559 ];
560 let resolver = MockResolver::new();
561 let verifier = ArcVerifier::new(resolver);
562 let result = verifier.validate_chain(&headers, b"body").await;
563 assert!(matches!(result.status, ArcResult::Fail { .. }));
564 }
565
566 #[tokio::test]
569 async fn duplicate_instances_fails() {
570 let headers = vec![
571 ("ARC-Authentication-Results", "i=1; spf=pass"),
572 ("ARC-Authentication-Results", "i=1; dkim=pass"),
573 (
574 "ARC-Message-Signature",
575 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
576 ),
577 (
578 "ARC-Seal",
579 "i=1; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
580 ),
581 ];
582 let resolver = MockResolver::new();
583 let verifier = ArcVerifier::new(resolver);
584 let result = verifier.validate_chain(&headers, b"body").await;
585 assert!(matches!(result.status, ArcResult::Fail { .. }));
586 }
587
588 #[tokio::test]
591 async fn instance_1_cv_pass_structure_fail() {
592 let headers = vec![
593 ("ARC-Authentication-Results", "i=1; spf=pass"),
594 (
595 "ARC-Message-Signature",
596 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
597 ),
598 (
599 "ARC-Seal",
600 "i=1; cv=pass; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
601 ),
602 ];
603 let resolver = MockResolver::new();
604 let verifier = ArcVerifier::new(resolver);
605 let result = verifier.validate_chain(&headers, b"body").await;
606 assert!(matches!(result.status, ArcResult::Fail { .. }));
607 }
608
609 #[tokio::test]
612 async fn ams_body_hash_mismatch_fails() {
613 let (headers_owned, resolver, _, _) = build_single_arc_set();
614 let headers: Vec<(&str, &str)> = headers_owned
615 .iter()
616 .map(|(n, v)| (n.as_str(), v.as_str()))
617 .collect();
618 let verifier = ArcVerifier::new(resolver);
619 let result = verifier.validate_chain(&headers, b"tampered body\r\n").await;
621 assert!(matches!(result.status, ArcResult::Fail { .. }));
622 }
623
624 #[tokio::test]
627 async fn seal_tampered_fails() {
628 let (mut headers_owned, resolver, body, _) = build_single_arc_set();
629 if let Some(aar) = headers_owned
631 .iter_mut()
632 .find(|(n, _)| n == "ARC-Authentication-Results")
633 {
634 aar.1 = "i=1; spf=fail smtp.mailfrom=evil.com".to_string();
635 }
636 let headers: Vec<(&str, &str)> = headers_owned
637 .iter()
638 .map(|(n, v)| (n.as_str(), v.as_str()))
639 .collect();
640 let verifier = ArcVerifier::new(resolver);
641 let result = verifier.validate_chain(&headers, &body).await;
642 assert!(matches!(result.status, ArcResult::Fail { .. }));
643 }
644
645 #[tokio::test]
648 async fn too_many_sets_fails() {
649 let headers = vec![
651 ("ARC-Authentication-Results", "i=51; spf=pass"),
652 ];
653 let resolver = MockResolver::new();
654 let verifier = ArcVerifier::new(resolver);
655 let result = verifier.validate_chain(&headers, b"body").await;
656 assert!(matches!(result.status, ArcResult::Fail { .. }));
657 }
658
659 #[tokio::test]
662 async fn three_sets_pass() {
663 let (pkcs8, pub_key) = gen_ed25519_keypair();
665 let body = b"test body\r\n";
666
667 let mut resolver = MockResolver::new();
668 resolver.add_txt(
669 "arc._domainkey.sealer.com",
670 vec![make_dns_key_record(&pub_key)],
671 );
672
673 let message_headers: Vec<(String, String)> = vec![
674 ("From".to_string(), "s@example.com".to_string()),
675 ("Subject".to_string(), "test".to_string()),
676 ];
677
678 let mut ordered_sets: Vec<(String, String, String)> = Vec::new();
681
682 for hop in 1..=3u32 {
683 let cv = if hop == 1 { "none" } else { "pass" };
684
685 let normalized = normalize_line_endings(body);
687 let canonicalized =
688 canonicalize_body(CanonicalizationMethod::Relaxed, &normalized);
689 let body_hash = compute_hash(Algorithm::Ed25519Sha256, &canonicalized);
690
691 let ams_raw_no_b = format!(
692 "i={}; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b=",
693 hop,
694 b64(&body_hash),
695 );
696
697 let non_arc: Vec<(&str, &str)> = message_headers
698 .iter()
699 .map(|(n, v)| (n.as_str(), v.as_str()))
700 .collect();
701
702 let selected = select_headers(
703 CanonicalizationMethod::Relaxed,
704 &["from".to_string(), "subject".to_string()],
705 &non_arc,
706 );
707 let mut ams_input = Vec::new();
708 for h in &selected {
709 ams_input.extend_from_slice(h.as_bytes());
710 }
711 let canon_ams =
712 canonicalize_header(CanonicalizationMethod::Relaxed, "arc-message-signature", &ams_raw_no_b);
713 ams_input.extend_from_slice(canon_ams.as_bytes());
714 let ams_sig = ed25519_sign(&pkcs8, &ams_input);
715 let ams_raw = format!(
716 "i={}; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b={}",
717 hop, b64(&body_hash), b64(&ams_sig),
718 );
719
720 let aar_raw = format!("i={}; spf=pass", hop);
721
722 let seal_raw_no_b = format!(
723 "i={}; cv={}; a=ed25519-sha256; d=sealer.com; s=arc; b=",
724 hop, cv,
725 );
726
727 let mut seal_input = Vec::new();
729 for prev in &ordered_sets {
731 let c = canonicalize_header(
732 CanonicalizationMethod::Relaxed,
733 "arc-authentication-results",
734 &prev.0,
735 );
736 seal_input.extend_from_slice(c.as_bytes());
737 let c = canonicalize_header(
738 CanonicalizationMethod::Relaxed,
739 "arc-message-signature",
740 &prev.1,
741 );
742 seal_input.extend_from_slice(c.as_bytes());
743 let c = canonicalize_header(
744 CanonicalizationMethod::Relaxed,
745 "arc-seal",
746 &prev.2,
747 );
748 seal_input.extend_from_slice(c.as_bytes());
749 }
750 let c = canonicalize_header(
752 CanonicalizationMethod::Relaxed,
753 "arc-authentication-results",
754 &aar_raw,
755 );
756 seal_input.extend_from_slice(c.as_bytes());
757 let c = canonicalize_header(
758 CanonicalizationMethod::Relaxed,
759 "arc-message-signature",
760 &ams_raw,
761 );
762 seal_input.extend_from_slice(c.as_bytes());
763 let c = canonicalize_header(
764 CanonicalizationMethod::Relaxed,
765 "arc-seal",
766 &seal_raw_no_b,
767 );
768 let seal_bytes = c.as_bytes();
769 if seal_bytes.ends_with(b"\r\n") {
770 seal_input.extend_from_slice(&seal_bytes[..seal_bytes.len() - 2]);
771 } else {
772 seal_input.extend_from_slice(seal_bytes);
773 }
774
775 let seal_sig = ed25519_sign(&pkcs8, &seal_input);
776 let seal_raw = format!(
777 "i={}; cv={}; a=ed25519-sha256; d=sealer.com; s=arc; b={}",
778 hop, cv, b64(&seal_sig),
779 );
780
781 ordered_sets.push((aar_raw.clone(), ams_raw.clone(), seal_raw.clone()));
783 }
784
785 let mut arc_headers: Vec<(String, String)> = Vec::new();
787 for (aar, ams, seal) in ordered_sets.iter().rev() {
788 arc_headers.push(("ARC-Seal".to_string(), seal.clone()));
789 arc_headers.push(("ARC-Message-Signature".to_string(), ams.clone()));
790 arc_headers.push(("ARC-Authentication-Results".to_string(), aar.clone()));
791 }
792
793 let mut all_headers: Vec<(String, String)> = arc_headers;
795 all_headers.extend(message_headers);
796
797 let headers: Vec<(&str, &str)> = all_headers
798 .iter()
799 .map(|(n, v)| (n.as_str(), v.as_str()))
800 .collect();
801 let verifier = ArcVerifier::new(resolver);
802 let result = verifier.validate_chain(&headers, body).await;
803 assert_eq!(result.status, ArcResult::Pass);
804 assert_eq!(result.oldest_pass, Some(0)); }
806
807 #[tokio::test]
810 async fn instance_2_cv_none_structure_fail() {
811 let headers = vec![
812 ("ARC-Authentication-Results", "i=1; spf=pass"),
813 (
814 "ARC-Message-Signature",
815 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
816 ),
817 (
818 "ARC-Seal",
819 "i=1; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
820 ),
821 ("ARC-Authentication-Results", "i=2; dkim=pass"),
822 (
823 "ARC-Message-Signature",
824 "i=2; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
825 ),
826 (
827 "ARC-Seal",
828 "i=2; cv=none; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
829 ),
830 ];
831 let resolver = MockResolver::new();
832 let verifier = ArcVerifier::new(resolver);
833 let result = verifier.validate_chain(&headers, b"body").await;
834 assert!(matches!(result.status, ArcResult::Fail { .. }));
835 }
836
837 #[tokio::test]
840 async fn highest_cv_fail_fast() {
841 let headers = vec![
842 ("ARC-Authentication-Results", "i=1; spf=pass"),
843 (
844 "ARC-Message-Signature",
845 "i=1; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==; bh=dGVzdA==; h=from",
846 ),
847 (
848 "ARC-Seal",
849 "i=1; cv=fail; a=rsa-sha256; d=ex.com; s=s1; b=dGVzdA==",
850 ),
851 ];
852 let resolver = MockResolver::new();
853 let verifier = ArcVerifier::new(resolver);
854 let result = verifier.validate_chain(&headers, b"body").await;
855 assert!(matches!(result.status, ArcResult::Fail { .. }));
856 }
857
858 #[tokio::test]
861 async fn oldest_pass_after_body_modification() {
862 let (pkcs8, pub_key) = gen_ed25519_keypair();
866
867 let original_body = b"original body\r\n";
868 let modified_body = b"modified body\r\n";
869 let message_headers: Vec<(String, String)> = vec![
870 ("From".to_string(), "s@example.com".to_string()),
871 ("Subject".to_string(), "test".to_string()),
872 ];
873
874 let normalized_orig = normalize_line_endings(original_body);
876 let canon_orig = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_orig);
877 let bh_orig = compute_hash(Algorithm::Ed25519Sha256, &canon_orig);
878
879 let ams1_raw_no_b = format!(
880 "i=1; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b=",
881 b64(&bh_orig),
882 );
883
884 let non_arc: Vec<(&str, &str)> = message_headers
885 .iter()
886 .map(|(n, v)| (n.as_str(), v.as_str()))
887 .collect();
888 let selected = select_headers(
889 CanonicalizationMethod::Relaxed,
890 &["from".to_string(), "subject".to_string()],
891 &non_arc,
892 );
893 let mut ams1_input = Vec::new();
894 for h in &selected {
895 ams1_input.extend_from_slice(h.as_bytes());
896 }
897 let canon_ams1 = canonicalize_header(
898 CanonicalizationMethod::Relaxed,
899 "arc-message-signature",
900 &ams1_raw_no_b,
901 );
902 ams1_input.extend_from_slice(canon_ams1.as_bytes());
903 let ams1_sig = ed25519_sign(&pkcs8, &ams1_input);
904 let ams1_raw = format!(
905 "i=1; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b={}",
906 b64(&bh_orig), b64(&ams1_sig),
907 );
908
909 let aar1_raw = "i=1; spf=pass".to_string();
910
911 let seal1_raw_no_b = "i=1; cv=none; a=ed25519-sha256; d=sealer.com; s=arc; b=".to_string();
913 let mut seal1_input = Vec::new();
914 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-authentication-results", &aar1_raw);
915 seal1_input.extend_from_slice(c.as_bytes());
916 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-message-signature", &ams1_raw);
917 seal1_input.extend_from_slice(c.as_bytes());
918 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-seal", &seal1_raw_no_b);
919 let sb = c.as_bytes();
920 if sb.ends_with(b"\r\n") {
921 seal1_input.extend_from_slice(&sb[..sb.len() - 2]);
922 } else {
923 seal1_input.extend_from_slice(sb);
924 }
925 let seal1_sig = ed25519_sign(&pkcs8, &seal1_input);
926 let seal1_raw = format!(
927 "i=1; cv=none; a=ed25519-sha256; d=sealer.com; s=arc; b={}",
928 b64(&seal1_sig),
929 );
930
931 let normalized_mod = normalize_line_endings(modified_body);
933 let canon_mod = canonicalize_body(CanonicalizationMethod::Relaxed, &normalized_mod);
934 let bh_mod = compute_hash(Algorithm::Ed25519Sha256, &canon_mod);
935
936 let ams2_raw_no_b = format!(
937 "i=2; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b=",
938 b64(&bh_mod),
939 );
940 let mut ams2_input = Vec::new();
941 for h in &selected {
942 ams2_input.extend_from_slice(h.as_bytes());
943 }
944 let canon_ams2 = canonicalize_header(
945 CanonicalizationMethod::Relaxed,
946 "arc-message-signature",
947 &ams2_raw_no_b,
948 );
949 ams2_input.extend_from_slice(canon_ams2.as_bytes());
950 let ams2_sig = ed25519_sign(&pkcs8, &ams2_input);
951 let ams2_raw = format!(
952 "i=2; a=ed25519-sha256; d=sealer.com; s=arc; c=relaxed/relaxed; h=from:subject; bh={}; b={}",
953 b64(&bh_mod), b64(&ams2_sig),
954 );
955
956 let aar2_raw = "i=2; arc=pass".to_string();
957
958 let seal2_raw_no_b = "i=2; cv=pass; a=ed25519-sha256; d=sealer.com; s=arc; b=".to_string();
960 let mut seal2_input = Vec::new();
961 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-authentication-results", &aar1_raw);
963 seal2_input.extend_from_slice(c.as_bytes());
964 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-message-signature", &ams1_raw);
965 seal2_input.extend_from_slice(c.as_bytes());
966 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-seal", &seal1_raw);
967 seal2_input.extend_from_slice(c.as_bytes());
968 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-authentication-results", &aar2_raw);
970 seal2_input.extend_from_slice(c.as_bytes());
971 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-message-signature", &ams2_raw);
972 seal2_input.extend_from_slice(c.as_bytes());
973 let c = canonicalize_header(CanonicalizationMethod::Relaxed, "arc-seal", &seal2_raw_no_b);
974 let sb = c.as_bytes();
975 if sb.ends_with(b"\r\n") {
976 seal2_input.extend_from_slice(&sb[..sb.len() - 2]);
977 } else {
978 seal2_input.extend_from_slice(sb);
979 }
980 let seal2_sig = ed25519_sign(&pkcs8, &seal2_input);
981 let seal2_raw = format!(
982 "i=2; cv=pass; a=ed25519-sha256; d=sealer.com; s=arc; b={}",
983 b64(&seal2_sig),
984 );
985
986 let mut all_headers: Vec<(String, String)> = vec![
988 ("ARC-Seal".to_string(), seal2_raw),
989 ("ARC-Message-Signature".to_string(), ams2_raw),
990 ("ARC-Authentication-Results".to_string(), aar2_raw),
991 ("ARC-Seal".to_string(), seal1_raw),
992 ("ARC-Message-Signature".to_string(), ams1_raw),
993 ("ARC-Authentication-Results".to_string(), aar1_raw),
994 ];
995 all_headers.extend(message_headers);
996
997 let headers: Vec<(&str, &str)> = all_headers
998 .iter()
999 .map(|(n, v)| (n.as_str(), v.as_str()))
1000 .collect();
1001
1002 let mut resolver = MockResolver::new();
1003 resolver.add_txt(
1004 "arc._domainkey.sealer.com",
1005 vec![make_dns_key_record(&pub_key)],
1006 );
1007 let verifier = ArcVerifier::new(resolver);
1008
1009 let result = verifier.validate_chain(&headers, modified_body).await;
1011 assert_eq!(result.status, ArcResult::Pass);
1012 assert_eq!(result.oldest_pass, Some(2));
1014 }
1015
1016 #[test]
1019 fn validate_structure_valid() {
1020 let sets = vec![
1021 ArcSet {
1022 instance: 1,
1023 aar: ArcAuthenticationResults {
1024 instance: 1,
1025 payload: "".to_string(),
1026 raw_header: "".to_string(),
1027 },
1028 ams: ArcMessageSignature {
1029 instance: 1,
1030 algorithm: Algorithm::RsaSha256,
1031 signature: vec![],
1032 body_hash: vec![],
1033 domain: "".to_string(),
1034 selector: "".to_string(),
1035 signed_headers: vec![],
1036 header_canonicalization: CanonicalizationMethod::Relaxed,
1037 body_canonicalization: CanonicalizationMethod::Relaxed,
1038 timestamp: Option::None,
1039 body_length: Option::None,
1040 raw_header: "".to_string(),
1041 },
1042 seal: ArcSeal {
1043 instance: 1,
1044 cv: ChainValidationStatus::None,
1045 algorithm: Algorithm::RsaSha256,
1046 signature: vec![],
1047 domain: "".to_string(),
1048 selector: "".to_string(),
1049 timestamp: Option::None,
1050 raw_header: "".to_string(),
1051 },
1052 },
1053 ];
1054 assert!(validate_structure(&sets).is_ok());
1055 }
1056}