Skip to main content

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