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}