mail_auth/arc/
verify.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use std::{
8    net::{IpAddr, Ipv4Addr, Ipv6Addr},
9    sync::Arc,
10    time::SystemTime,
11};
12
13use crate::{
14    common::{
15        crypto::HashAlgorithm,
16        headers::Header,
17        verify::{DomainKey, VerifySignature},
18    },
19    dkim::{verify::Verifier, Canonicalization},
20    ArcOutput, AuthenticatedMessage, DkimResult, Error, MessageAuthenticator, Parameters,
21    ResolverCache, Txt, MX,
22};
23
24use super::{ChainValidation, Set};
25
26impl MessageAuthenticator {
27    /// Verifies ARC headers of an RFC5322 message.
28    pub async fn verify_arc<'x, TXT, MXX, IPV4, IPV6, PTR>(
29        &self,
30        params: impl Into<Parameters<'x, &'x AuthenticatedMessage<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
31    ) -> ArcOutput<'x>
32    where
33        TXT: ResolverCache<String, Txt> + 'x,
34        MXX: ResolverCache<String, Arc<Vec<MX>>> + 'x,
35        IPV4: ResolverCache<String, Arc<Vec<Ipv4Addr>>> + 'x,
36        IPV6: ResolverCache<String, Arc<Vec<Ipv6Addr>>> + 'x,
37        PTR: ResolverCache<IpAddr, Arc<Vec<String>>> + 'x,
38    {
39        let params = params.into();
40        let message = params.params;
41        let arc_headers = message.ams_headers.len();
42        if arc_headers == 0 {
43            return ArcOutput::default();
44        } else if arc_headers > 50 {
45            return ArcOutput::default().with_result(DkimResult::Fail(Error::ArcChainTooLong));
46        } else if (arc_headers != message.as_headers.len())
47            || (arc_headers != message.aar_headers.len())
48        {
49            return ArcOutput::default().with_result(DkimResult::Fail(Error::ArcBrokenChain));
50        }
51
52        let now = SystemTime::now()
53            .duration_since(SystemTime::UNIX_EPOCH)
54            .map(|d| d.as_secs())
55            .unwrap_or(0);
56
57        let mut output = ArcOutput {
58            result: DkimResult::None,
59            set: Vec::with_capacity(message.aar_headers.len() / 3),
60        };
61
62        // Group ARC headers in sets
63        for (pos, ((seal_, signature_), results_)) in message
64            .as_headers
65            .iter()
66            .zip(message.ams_headers.iter())
67            .zip(message.aar_headers.iter())
68            .enumerate()
69        {
70            let seal = match &seal_.header {
71                Ok(seal) => seal,
72                Err(err) => return output.with_result(DkimResult::Neutral(err.clone())),
73            };
74            let signature = match &signature_.header {
75                Ok(signature) => signature,
76                Err(err) => return output.with_result(DkimResult::Neutral(err.clone())),
77            };
78            let results = match &results_.header {
79                Ok(results) => results,
80                Err(err) => return output.with_result(DkimResult::Neutral(err.clone())),
81            };
82
83            if output.result == DkimResult::None {
84                if (seal.i as usize != (pos + 1))
85                    || (signature.i as usize != (pos + 1))
86                    || (results.i as usize != (pos + 1))
87                {
88                    output.result = DkimResult::Fail(Error::ArcInvalidInstance((pos + 1) as u32));
89                } else if (pos == 0 && seal.cv != ChainValidation::None)
90                    || (pos > 0 && seal.cv != ChainValidation::Pass)
91                {
92                    output.result = DkimResult::Fail(Error::ArcInvalidCV);
93                } else if pos == arc_headers - 1 {
94                    // Validate last signature in the chain
95                    if signature.x == 0 || (signature.x > signature.t && signature.x > now) {
96                        // Validate body hash
97                        let ha = HashAlgorithm::from(signature.a);
98                        let bh = &message
99                            .body_hashes
100                            .iter()
101                            .find(|(c, h, l, _)| {
102                                c == &signature.cb && h == &ha && l == &signature.l
103                            })
104                            .unwrap()
105                            .3;
106                        if bh != &signature.bh {
107                            output.result = DkimResult::Neutral(Error::FailedBodyHashMatch);
108                        }
109                    } else {
110                        output.result = DkimResult::Neutral(Error::SignatureExpired);
111                    }
112                }
113            }
114
115            output.set.push(Set {
116                signature: Header::new(signature_.name, signature_.value, signature),
117                seal: Header::new(seal_.name, seal_.value, seal),
118                results: Header::new(results_.name, results_.value, results),
119            });
120        }
121
122        if output.result != DkimResult::None {
123            return output;
124        }
125
126        // Validate ARC Set
127        let arc_set = output.set.last().unwrap();
128        let header = &arc_set.signature;
129        let signature = &header.header;
130
131        // Hash headers
132        let dkim_hdr_value = header.value.strip_signature();
133        let mut headers = message.signed_headers(&signature.h, header.name, &dkim_hdr_value);
134
135        // Obtain record
136        let record = match self
137            .txt_lookup::<DomainKey>(signature.domain_key(), params.cache_txt)
138            .await
139        {
140            Ok(record) => record,
141            Err(err) => {
142                return output.with_result(err.into());
143            }
144        };
145
146        // Verify signature
147        if let Err(err) = record.verify(&mut headers, *signature, signature.ch) {
148            return output.with_result(DkimResult::Fail(err));
149        }
150
151        // Validate ARC Seals
152        for (pos, set) in output.set.iter().enumerate().rev() {
153            // Obtain record
154            let header = &set.seal;
155            let seal = &header.header;
156            let record = match self
157                .txt_lookup::<DomainKey>(seal.domain_key(), params.cache_txt)
158                .await
159            {
160                Ok(record) => record,
161                Err(err) => {
162                    return output.with_result(err.into());
163                }
164            };
165
166            // Build Seal headers
167            let seal_signature = header.value.strip_signature();
168            let mut headers = output
169                .set
170                .iter()
171                .take(pos)
172                .flat_map(|set| {
173                    [
174                        (set.results.name, set.results.value),
175                        (set.signature.name, set.signature.value),
176                        (set.seal.name, set.seal.value),
177                    ]
178                })
179                .chain([
180                    (set.results.name, set.results.value),
181                    (set.signature.name, set.signature.value),
182                    (set.seal.name, &seal_signature),
183                ]);
184
185            // Verify ARC Seal
186            if let Err(err) = record.verify(&mut headers, *seal, Canonicalization::Relaxed) {
187                return output.with_result(DkimResult::Fail(err));
188            }
189        }
190
191        // ARC Validation successful
192        output.with_result(DkimResult::Pass)
193    }
194}
195
196#[cfg(test)]
197#[allow(unused)]
198mod test {
199    use std::{
200        fs,
201        path::PathBuf,
202        time::{Duration, Instant},
203    };
204
205    use mail_parser::MessageParser;
206
207    use crate::{
208        common::{cache::test::DummyCaches, parse::TxtRecordParser, verify::DomainKey},
209        dkim::verify::test::new_cache,
210        AuthenticatedMessage, DkimResult, MessageAuthenticator,
211    };
212
213    #[tokio::test]
214    async fn arc_verify() {
215        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
216        test_dir.push("resources");
217        test_dir.push("arc");
218        let resolver = MessageAuthenticator::new_system_conf().unwrap();
219
220        for file_name in fs::read_dir(&test_dir).unwrap() {
221            let file_name = file_name.unwrap().path();
222            /*if !file_name.to_str().unwrap().contains("002") {
223                continue;
224            }*/
225            println!("file {}", file_name.to_str().unwrap());
226
227            let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
228            let (dns_records, raw_message) = test.split_once("\n\n").unwrap();
229            let caches = new_cache(dns_records);
230            let raw_message = raw_message.replace('\n', "\r\n");
231            let message = AuthenticatedMessage::parse(raw_message.as_bytes()).unwrap();
232            assert_eq!(
233                message,
234                AuthenticatedMessage::from_parsed(
235                    &MessageParser::new().parse(&raw_message).unwrap(),
236                    true
237                )
238            );
239
240            let arc = resolver.verify_arc(caches.parameters(&message)).await;
241            assert_eq!(arc.result(), &DkimResult::Pass);
242
243            let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
244            assert!(dkim.iter().any(|o| o.result() == &DkimResult::Pass));
245        }
246    }
247}