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