1use crate::header::{ARCMessageSignatureHeader, ARCSealHeader};
2use crate::{verify_email_header, DKIMError, ParsedEmail, Signer};
3use dns_resolver::Resolver;
4use mailparsing::{ARCAuthenticationResults, AuthenticationResult, AuthenticationResults, Header};
5use std::collections::BTreeMap;
6use std::str::FromStr;
7
8pub const MAX_ARC_INSTANCE: u8 = 50;
9pub const ARC_MESSAGE_SIGNATURE_HEADER_NAME: &str = "ARC-Message-Signature";
10pub const ARC_SEAL_HEADER_NAME: &str = "ARC-Seal";
11pub const ARC_AUTHENTICATION_RESULTS_HEADER_NAME: &str = "ARC-Authentication-Results";
12
13#[derive(Debug, PartialEq, Eq, Clone, Copy)]
14pub enum ChainValidationStatus {
15 None,
16 Fail,
17 Pass,
18}
19
20impl std::fmt::Display for ChainValidationStatus {
21 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
22 match self {
23 Self::None => write!(fmt, "none"),
24 Self::Fail => write!(fmt, "fail"),
25 Self::Pass => write!(fmt, "pass"),
26 }
27 }
28}
29
30impl FromStr for ChainValidationStatus {
31 type Err = String;
32 fn from_str(s: &str) -> Result<ChainValidationStatus, String> {
33 if s.eq_ignore_ascii_case("none") {
34 Ok(ChainValidationStatus::None)
35 } else if s.eq_ignore_ascii_case("fail") {
36 Ok(ChainValidationStatus::Fail)
37 } else if s.eq_ignore_ascii_case("pass") {
38 Ok(ChainValidationStatus::Pass)
39 } else {
40 Err(format!("invalid ChainValidationStatus {s}"))
41 }
42 }
43}
44
45#[derive(Debug)]
46pub struct ARC {
47 pub sets: Vec<ARCSet>,
48 pub last_validated_instance: u8,
50 pub issues: Vec<ARCIssue>,
51}
52
53#[derive(Debug)]
54pub struct ARCIssue {
55 pub reason: String,
56 pub error: Option<DKIMError>,
57 pub header: Option<Header<'static>>,
58}
59
60impl ARC {
61 pub fn chain_validation_status(&self) -> ChainValidationStatus {
62 if self.issues.is_empty() {
63 if self.sets.is_empty() {
64 ChainValidationStatus::None
65 } else if self.last_validated_instance as usize == self.sets.len() {
66 ChainValidationStatus::Pass
67 } else {
68 ChainValidationStatus::Fail
69 }
70 } else {
71 ChainValidationStatus::Fail
72 }
73 }
74
75 pub fn authentication_result(&self) -> AuthenticationResult {
76 let status = self.chain_validation_status();
77
78 let mut props = BTreeMap::new();
79
80 if !self.sets.is_empty() {
81 props.insert(
82 "header.oldest-pass".to_string(),
83 if status == ChainValidationStatus::Fail {
84 self.last_validated_instance
85 } else {
86 0
87 }
88 .to_string(),
89 );
90 }
91
92 AuthenticationResult {
93 method: "arc".to_string(),
94 method_version: None,
95 result: status.to_string(),
96 reason: self.issues.first().map(|issue| issue.reason.to_string()),
97 props,
98 }
99 }
100
101 pub fn seal(
102 &self,
103 email: &ParsedEmail<'_>,
104 auth_results: AuthenticationResults,
105 signer: &Signer,
106 ) -> Result<Vec<Header<'static>>, DKIMError> {
107 if self.chain_validation_status() == ChainValidationStatus::Fail {
108 return Ok(vec![]);
109 }
110
111 let instance = self.sets.len() as u8 + 1;
112
113 let ams = signer.sign_impl(
114 email,
115 ARC_MESSAGE_SIGNATURE_HEADER_NAME,
116 &[("i", instance.to_string())],
117 None,
118 )?;
119 let (ams, _) = Header::parse(ams)?;
120
121 let aar = ARCAuthenticationResults {
122 instance,
123 serv_id: auth_results.serv_id,
124 version: auth_results.version,
125 results: auth_results.results,
126 };
127 let aar = Header::new(ARC_AUTHENTICATION_RESULTS_HEADER_NAME, aar);
128
129 let mut seal_headers = vec![];
130 for arc_set in &self.sets {
131 seal_headers.push(&arc_set.aar_header);
132 seal_headers.push(&arc_set.sig_header);
133 seal_headers.push(&arc_set.seal_header);
134 }
135 seal_headers.push(&aar);
136 seal_headers.push(&ams);
137
138 let seal = signer.sign_impl(
139 email,
140 ARC_SEAL_HEADER_NAME,
141 &[
142 ("i", instance.to_string()),
143 (
144 "cv",
145 if self.sets.is_empty() { "none" } else { "pass" }.to_string(),
146 ),
147 ],
148 Some(seal_headers),
149 )?;
150 let (seal, _) = Header::parse(seal)?;
151
152 Ok(vec![aar, ams, seal])
153 }
154
155 pub async fn verify(email: &ParsedEmail<'_>, resolver: &dyn Resolver) -> Self {
156 let mut seals = BTreeMap::new();
157 let mut sigs = BTreeMap::new();
158 let mut aars = BTreeMap::new();
159
160 let headers = email.get_headers();
161 let mut issues = vec![];
162
163 for hdr in headers.iter_named(ARC_SEAL_HEADER_NAME) {
164 match ARCSealHeader::parse(hdr.get_raw_value()) {
165 Ok(seal) => {
166 let instance = seal.arc_instance().expect("validated by parse");
167 seals
168 .entry(instance)
169 .or_insert_with(Vec::new)
170 .push((seal, hdr.to_owned()));
171 }
172 Err(err) => {
173 issues.push(ARCIssue {
174 reason: format!(
175 "An {ARC_SEAL_HEADER_NAME} header could not be parsed: {err:#}"
176 ),
177 error: Some(err),
178 header: Some(hdr.to_owned()),
179 });
180 }
181 }
182 }
183
184 for hdr in headers.iter_named(ARC_MESSAGE_SIGNATURE_HEADER_NAME) {
185 match ARCMessageSignatureHeader::parse(hdr.get_raw_value()) {
186 Ok(sig) => {
187 let instance = sig.arc_instance().expect("validated by parse");
188 sigs.entry(instance)
189 .or_insert_with(Vec::new)
190 .push((sig, hdr.to_owned()));
191 }
192 Err(err) => {
193 issues.push(ARCIssue {
194 reason: format!(
195 "An {ARC_MESSAGE_SIGNATURE_HEADER_NAME} header could not be parsed: {err:#}"
196 ),
197 error: Some(err),
198 header: Some(hdr.to_owned()),
199 });
200 }
201 }
202 }
203
204 for hdr in headers.iter_named(ARC_AUTHENTICATION_RESULTS_HEADER_NAME) {
205 match hdr.as_arc_authentication_results() {
206 Ok(aar) => {
207 if aar.instance == 0 || aar.instance > MAX_ARC_INSTANCE {
208 issues.push(ARCIssue {
209 reason: format!(
210 "An {ARC_AUTHENTICATION_RESULTS_HEADER_NAME} header \
211 has an invalid instance value"
212 ),
213 error: Some(DKIMError::InvalidARCInstance),
214 header: Some(hdr.to_owned()),
215 });
216 continue;
217 }
218 aars.entry(aar.instance)
219 .or_insert_with(Vec::new)
220 .push((aar, hdr.to_owned()));
221 }
222 Err(err) => {
223 issues.push(ARCIssue {
224 reason: format!(
225 "An {ARC_AUTHENTICATION_RESULTS_HEADER_NAME} header \
226 could not be parsed: {err:#}"
227 ),
228 error: Some(err.into()),
229 header: Some(hdr.to_owned()),
230 });
231 }
232 }
233 }
234
235 let mut arc_sets = BTreeMap::new();
236 for instance in 1..=MAX_ARC_INSTANCE {
237 match (
238 seals.get_mut(&instance),
239 sigs.get_mut(&instance),
240 aars.get_mut(&instance),
241 ) {
242 (Some(seal), Some(sig), Some(aar)) => {
243 if seal.len() > 1 || sig.len() > 1 || aar.len() > 1 {
244 let mut duplicates = vec![];
245 if seal.len() > 1 {
246 duplicates.push(ARC_SEAL_HEADER_NAME);
247 }
248 if sig.len() > 1 {
249 duplicates.push(ARC_MESSAGE_SIGNATURE_HEADER_NAME);
250 }
251 if aar.len() > 1 {
252 duplicates.push(ARC_AUTHENTICATION_RESULTS_HEADER_NAME);
253 }
254 let duplicates = duplicates.join(", ");
255 issues.push(ARCIssue {
256 reason: format!(
257 "There are duplicate {duplicates} header(s) \
258 for instance {instance}"
259 ),
260 error: Some(DKIMError::DuplicateARCInstance(instance)),
261 header: None,
262 });
263 continue;
264 }
265
266 let (seal, seal_header) = seal.pop().expect("one");
267 let (sig, sig_header) = sig.pop().expect("one");
268 let (aar, aar_header) = aar.pop().expect("one");
269
270 arc_sets.insert(
271 instance,
272 ARCSet {
273 seal,
274 seal_header,
275 sig,
276 sig_header,
277 aar,
278 aar_header,
279 },
280 );
281 }
282 (None, None, None) => {
283 }
286 _ => {
287 issues.push(ARCIssue {
289 reason: format!(
290 "The ARC Set with instance {instance} is \
291 missing some of its constituent headers"
292 ),
293 error: Some(DKIMError::MissingARCInstance(instance)),
294 header: None,
295 });
296 }
297 }
298 }
299
300 for instance in 2..=MAX_ARC_INSTANCE {
302 if arc_sets.contains_key(&instance) {
303 let prior = instance - 1;
304 if !arc_sets.contains_key(&prior) {
305 issues.push(ARCIssue {
306 reason: format!("The ARC Set with instance {prior} is missing"),
307 error: Some(DKIMError::MissingARCInstance(prior)),
308 header: None,
309 });
310 }
311 }
312 }
313
314 let mut arc = ARC {
315 sets: arc_sets.into_iter().map(|(_k, set)| set).collect(),
316 last_validated_instance: 0,
317 issues,
318 };
319
320 arc.validate_signatures(email, resolver).await;
321
322 arc
323 }
324
325 pub async fn validate_signatures(&mut self, email: &ParsedEmail<'_>, resolver: &dyn Resolver) {
326 let mut seal_headers = vec![];
327
328 for arc_set in &self.sets {
329 if let Err(err) = verify_email_header(
330 resolver,
331 ARC_MESSAGE_SIGNATURE_HEADER_NAME,
332 &arc_set.sig,
333 email,
334 )
335 .await
336 {
337 self.issues.push(ARCIssue {
338 reason: format!(
339 "The {ARC_MESSAGE_SIGNATURE_HEADER_NAME} for \
340 instance {} failed to validate",
341 arc_set.instance()
342 ),
343 error: Some(err),
344 header: Some(arc_set.sig_header.clone()),
345 });
346
347 break;
348 }
349
350 seal_headers.push(&arc_set.aar_header);
351 seal_headers.push(&arc_set.sig_header);
352 if let Err(err) = arc_set.seal.verify(resolver, &seal_headers).await {
357 self.issues.push(ARCIssue {
358 reason: format!(
359 "The {ARC_SEAL_HEADER_NAME} for instance {} failed to validate",
360 arc_set.instance()
361 ),
362 error: Some(err),
363 header: Some(arc_set.seal_header.clone()),
364 });
365
366 break;
367 }
368 seal_headers.push(&arc_set.seal_header);
370
371 match arc_set.seal.parse_tag::<ChainValidationStatus>("cv") {
372 Ok(Some(ChainValidationStatus::Pass)) => {
373 if arc_set.instance() == 1 {
374 self.issues.push(ARCIssue {
375 reason: format!(
376 "The {ARC_SEAL_HEADER_NAME} for instance {} is \
377 marked as cv=pass but that is invalid",
378 arc_set.instance()
379 ),
380 error: None,
381 header: Some(arc_set.seal_header.clone()),
382 });
383
384 break;
385 }
386 }
387 Ok(Some(ChainValidationStatus::None)) => {
388 if arc_set.instance() > 1 {
389 self.issues.push(ARCIssue {
390 reason: format!(
391 "The {ARC_SEAL_HEADER_NAME} for instance {} is \
392 marked as cv=none but that is invalid",
393 arc_set.instance()
394 ),
395 error: None,
396 header: Some(arc_set.seal_header.clone()),
397 });
398
399 break;
400 }
401 }
402 Ok(Some(ChainValidationStatus::Fail)) => {
403 self.issues.push(ARCIssue {
404 reason: format!(
405 "The {ARC_SEAL_HEADER_NAME} for instance {} is \
406 marked as failing its chain validation",
407 arc_set.instance()
408 ),
409 error: None,
410 header: Some(arc_set.seal_header.clone()),
411 });
412
413 break;
414 }
415 Ok(None) => {
416 self.issues.push(ARCIssue {
417 reason: format!(
418 "The {ARC_SEAL_HEADER_NAME} for instance {} is \
419 missing its chain validation status",
420 arc_set.instance()
421 ),
422 error: None,
423 header: Some(arc_set.seal_header.clone()),
424 });
425 break;
426 }
427 Err(err) => {
428 self.issues.push(ARCIssue {
429 reason: format!(
430 "The {ARC_SEAL_HEADER_NAME} for instance {} has \
431 an invalid chain validation status",
432 arc_set.instance()
433 ),
434 error: Some(err),
435 header: Some(arc_set.seal_header.clone()),
436 });
437 break;
438 }
439 }
440
441 self.last_validated_instance = arc_set.instance();
442 }
443 }
444}
445
446#[derive(Debug)]
447pub struct ARCSet {
448 pub aar: ARCAuthenticationResults,
449 pub aar_header: Header<'static>,
450 pub seal: ARCSealHeader,
451 pub seal_header: Header<'static>,
452 pub sig: ARCMessageSignatureHeader,
453 pub sig_header: Header<'static>,
454}
455
456impl ARCSet {
457 pub fn instance(&self) -> u8 {
458 self.aar.instance
459 }
460}
461
462#[cfg(test)]
463mod test {
464 use super::*;
465 use crate::roundtrip_test::{load_rsa_key, TEST_ZONE};
466 use crate::SignerBuilder;
467 use chrono::{DateTime, TimeZone, Utc};
468 use dns_resolver::TestResolver;
469
470 const EXAMPLE_MESSAGE: &str = include_str!("../test/arc-example.eml");
471 const EXAMPLE_MESSAGE_2: &str = include_str!("../test/arc-example-2.eml");
472
473 const FM3_ZONE_NAME: &str = "fm3._domainkey.messagingengine.com";
474 const FM3_ZONE_TXT: &str = "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC\
475 AQ8AMIIBCgKCAQEA3TntGwdEtmIx+H8Etk1IgA2gLzy9v22TO+BcTUmUFaURWSG413g+VIt\
476 m86ntW1bfbgFk/ArrTVAzQxgynoCQky3VXMXl2qEKgGSrLv+QaNvbebVDZI6VZX8D5+aJIN\
477 3sCSVY1eXA4x6LbPZ8pAqIAuAhtfXc7rVKbELqlEaUMrQ+ovyjF4R6gfL621BKdLeTF89/k\
478 bqJhLwmgtzok6UBUzexDDBhZ0gfGw331J+7aqdJLWUCQv6iE3zkI4myyEcMrgWxRjdZ861x\
479 374pNzady/B688A5i4BHoVnBJBuLEYfS1gTCC/7SB6U5AdEin3P0/+DqSH36cu8+MvAZ1C7E2wIDAQAB";
480
481 #[tokio::test]
482 async fn test_parse_example_1() {
483 let email = ParsedEmail::parse(EXAMPLE_MESSAGE.replace('\n', "\r\n")).unwrap();
484 let resolver = TestResolver::default().with_txt(FM3_ZONE_NAME, FM3_ZONE_TXT);
485 let arc = ARC::verify(&email, &resolver).await;
486 eprintln!("{:#?}", arc.issues);
487 assert_eq!(arc.chain_validation_status(), ChainValidationStatus::Pass);
488 }
489
490 #[tokio::test]
491 async fn test_parse_example_2() {
492 let email = ParsedEmail::parse(EXAMPLE_MESSAGE_2.replace('\n', "\r\n")).unwrap();
493 let resolver = TestResolver::default().with_txt(FM3_ZONE_NAME, FM3_ZONE_TXT);
494 let arc = ARC::verify(&email, &resolver).await;
495 eprintln!("{:#?}", arc.issues);
496 assert_eq!(arc.chain_validation_status(), ChainValidationStatus::Pass);
497 }
498
499 #[tokio::test]
500 async fn test_parse_no_sets() {
501 let email = ParsedEmail::parse("Subject: hello\r\n\r\nHello\r\n").unwrap();
502 let resolver = TestResolver::default();
503 let arc = ARC::verify(&email, &resolver).await;
504 eprintln!("{:#?}", arc.issues);
505 assert_eq!(arc.chain_validation_status(), ChainValidationStatus::None);
506 }
507
508 fn fixed_time() -> DateTime<Utc> {
509 Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap()
510 }
511
512 #[tokio::test]
513 async fn seal_1() {
514 let resolver = TestResolver::default().with_txt("s20._domainkey.example.com", TEST_ZONE);
515
516 let mut email_content = "Subject: hello\r\n\r\nHello\r\n".to_string();
517
518 let signer = SignerBuilder::new()
519 .with_signed_headers(["From", "Subject"])
520 .unwrap()
521 .with_private_key(load_rsa_key())
522 .with_selector("s20")
523 .with_signing_domain("example.com")
524 .with_time(fixed_time())
525 .with_over_signing(true)
526 .build()
527 .unwrap();
528
529 for instance in 1..=5 {
530 let email = ParsedEmail::parse(email_content.as_str()).unwrap();
531 let arc = ARC::verify(&email, &resolver).await;
532 assert_eq!(
533 arc.chain_validation_status(),
534 if instance == 1 {
535 ChainValidationStatus::None
536 } else {
537 ChainValidationStatus::Pass
538 }
539 );
540 let headers = arc
541 .seal(
542 &email,
543 AuthenticationResults {
544 serv_id: "localhost".to_string(),
545 version: None,
546 results: vec![arc.authentication_result()],
547 },
548 &signer,
549 )
550 .unwrap();
551
552 let mut sealed = String::new();
553 for hdr in headers {
554 sealed.push_str(&hdr.to_header_string());
555 }
556 sealed.push_str(&email_content);
557
558 eprintln!("{sealed}\ninstance={instance}");
559
560 let arc2 = ARC::verify(&ParsedEmail::parse(sealed.as_str()).unwrap(), &resolver).await;
561 for issue in &arc2.issues {
562 eprintln!("{}", issue.reason);
563 eprintln!(
564 " header: {:?}",
565 issue.header.as_ref().map(|h| h.to_header_string())
566 );
567 eprintln!(" err: {:#?}", issue.error.as_ref());
568 }
569 assert_eq!(arc2.chain_validation_status(), ChainValidationStatus::Pass);
570
571 email_content = sealed;
572 }
573 }
574}