kumo_dkim/
arc.rs

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    /// The instance number of the oldest pass that still validates
49    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                    // Not an error unless there are gaps; we'll check
296                    // for that below
297                }
298                _ => {
299                    // One or more are missing
300                    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        // Ensure that the keys are contiguous
313        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            // Don't add the seal header yet, as it is implicitly
365            // processed by the seal.verify routine
366
367            // Verify the Seal
368            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            // now we can add the seal header for any additional passes
381            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}