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