1use 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 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 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 if signature.x == 0 || (signature.x > signature.t && signature.x > now) {
93 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 let arc_set = output.set.last().unwrap();
125 let header = &arc_set.signature;
126 let signature = &header.header;
127
128 let dkim_hdr_value = header.value.strip_signature();
130 let mut headers = message.signed_headers(&signature.h, header.name, &dkim_hdr_value);
131
132 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 if let Err(err) = record.verify(&mut headers, *signature, signature.ch) {
145 return output.with_result(DkimResult::Fail(err));
146 }
147
148 for (pos, set) in output.set.iter().enumerate().rev() {
150 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 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 if let Err(err) = record.verify(&mut headers, *seal, Canonicalization::Relaxed) {
184 return output.with_result(DkimResult::Fail(err));
185 }
186 }
187
188 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 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}