1use 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 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 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 if signature.x == 0 || (signature.x > signature.t && signature.x > now) {
96 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 let arc_set = output.set.last().unwrap();
128 let header = &arc_set.signature;
129 let signature = &header.header;
130
131 let dkim_hdr_value = header.value.strip_signature();
133 let mut headers = message.signed_headers(&signature.h, header.name, &dkim_hdr_value);
134
135 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 if let Err(err) = record.verify(&mut headers, *signature, signature.ch) {
148 return output.with_result(DkimResult::Fail(err));
149 }
150
151 for (pos, set) in output.set.iter().enumerate().rev() {
153 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 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 if let Err(err) = record.verify(&mut headers, *seal, Canonicalization::Relaxed) {
187 return output.with_result(DkimResult::Fail(err));
188 }
189 }
190
191 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 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}