1use crate::arc::{ARC_SEAL_HEADER_NAME, MAX_ARC_INSTANCE};
2use crate::{parser, DKIMError, HeaderList};
3use dns_resolver::{Name, Resolver};
4use indexmap::map::IndexMap;
5use mailparsing::Header;
6use std::str::FromStr;
7use textwrap::core::Word;
8
9pub(crate) const DKIM_SIGNATURE_HEADER_NAME: &str = "DKIM-Signature";
10const SIGN_EXPIRATION_DRIFT_MINS: i64 = 15;
11
12#[derive(Clone, Debug, Default)]
13pub struct TaggedHeader {
14 tags: IndexMap<String, parser::Tag>,
15 raw_bytes: String,
16}
17
18impl TaggedHeader {
19 pub fn parse(value: &str) -> Result<Self, DKIMError> {
20 let (_, tags) = parser::tag_list(value)
21 .map_err(|err| DKIMError::SignatureSyntaxError(err.to_string()))?;
22
23 let mut tags_map = IndexMap::new();
24 for tag in &tags {
25 tags_map.insert(tag.name.clone(), tag.clone());
26 }
27 Ok(Self {
28 tags: tags_map,
29 raw_bytes: value.to_owned(),
30 })
31 }
32
33 pub fn get_tag(&self, name: &str) -> Option<&str> {
34 self.tags.get(name).map(|v| v.value.as_str())
35 }
36
37 pub fn parse_tag<R>(&self, name: &str) -> Result<Option<R>, DKIMError>
40 where
41 R: FromStr,
42 <R as FromStr>::Err: std::fmt::Display,
43 {
44 match self.get_tag(name) {
45 None => Ok(None),
46 Some(value) => {
47 let value: R = value.parse().map_err(|err| {
48 DKIMError::SignatureSyntaxError(format!(
49 "invalid \"{name}\" tag value: {err:#}"
50 ))
51 })?;
52 Ok(Some(value))
53 }
54 }
55 }
56
57 pub fn get_raw_tag(&self, name: &str) -> Option<&str> {
58 self.tags.get(name).map(|v| v.raw_value.as_str())
59 }
60
61 pub fn get_required_tag(&self, name: &str) -> &str {
62 match self.get_tag(name) {
65 Some(value) => value,
66 None => panic!("required tag {name} is not present"),
67 }
68 }
69
70 pub fn get_required_raw_tag(&self, name: &str) -> &str {
71 match self.get_raw_tag(name) {
74 Some(value) => value,
75 None => panic!("required tag {name} is not present"),
76 }
77 }
78
79 pub fn raw(&self) -> &str {
80 &self.raw_bytes
81 }
82
83 pub fn arc_instance(&self) -> Result<u8, DKIMError> {
84 let instance = self
85 .get_required_tag("i")
86 .parse::<u8>()
87 .map_err(|_| DKIMError::InvalidARCInstance)?;
88
89 if instance == 0 || instance > MAX_ARC_INSTANCE {
90 return Err(DKIMError::InvalidARCInstance);
91 }
92
93 Ok(instance)
94 }
95
96 fn serialize(&self) -> String {
98 let mut out = String::new();
99
100 for (key, tag) in &self.tags {
101 let mut value = &tag.value;
102 let value_storage;
103
104 if !out.is_empty() {
105 if key == "b" {
106 out.push_str("\r\n");
114 } else if key == "h" {
115 out.push_str("\r\n");
119 value_storage = textwrap::fill(
120 value,
121 textwrap::Options::new(75)
122 .initial_indent("")
123 .line_ending(textwrap::LineEnding::CRLF)
124 .word_separator(textwrap::WordSeparator::Custom(|line| {
125 let mut start = 0;
126 let mut prev_was_colon = false;
127 let mut char_indices = line.char_indices();
128
129 Box::new(std::iter::from_fn(move || {
130 for (idx, ch) in char_indices.by_ref() {
131 if ch == ':' {
132 prev_was_colon = true;
133 } else if prev_was_colon {
134 prev_was_colon = false;
135 let word = Word::from(&line[start..idx]);
136 start = idx;
137
138 return Some(word);
139 }
140 }
141 if start < line.len() {
142 let word = Word::from(&line[start..]);
143 start = line.len();
144 return Some(word);
145 }
146 None
147 }))
148 }))
149 .word_splitter(textwrap::WordSplitter::NoHyphenation)
150 .subsequent_indent("\t"),
151 );
152 value = &value_storage;
153 } else {
154 out.push(' ');
155 }
156 }
157 out.push_str(key);
158 out.push('=');
159 out.push_str(value);
160 out.push(';');
161 }
162 textwrap::fill(
163 &out,
164 textwrap::Options::new(75)
165 .initial_indent("")
166 .line_ending(textwrap::LineEnding::CRLF)
167 .word_separator(textwrap::WordSeparator::AsciiSpace)
168 .word_splitter(textwrap::WordSplitter::NoHyphenation)
169 .subsequent_indent("\t"),
170 )
171 }
172
173 fn check_common_tags(&self) -> Result<(), DKIMError> {
175 if !self
177 .get_required_tag("h")
178 .split(':')
179 .any(|h| h.eq_ignore_ascii_case("from"))
180 {
181 return Err(DKIMError::FromFieldNotSigned);
182 }
183
184 if let Some(query_method) = self.get_tag("q") {
185 if query_method != "dns/txt" {
186 return Err(DKIMError::UnsupportedQueryMethod);
187 }
188 }
189
190 if let Some(expiration) = self.get_tag("x") {
192 let mut expiration =
193 chrono::DateTime::from_timestamp(expiration.parse::<i64>().unwrap_or_default(), 0)
194 .ok_or(DKIMError::SignatureExpired)?;
195 expiration += chrono::Duration::try_minutes(SIGN_EXPIRATION_DRIFT_MINS)
196 .expect("drift to be in-range");
197 let now = chrono::Utc::now();
198 if now > expiration {
199 return Err(DKIMError::SignatureExpired);
200 }
201 }
202
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Default)]
208pub(crate) struct DKIMHeader {
209 tagged: TaggedHeader,
210}
211
212impl std::ops::Deref for DKIMHeader {
213 type Target = TaggedHeader;
214 fn deref(&self) -> &TaggedHeader {
215 &self.tagged
216 }
217}
218impl std::ops::DerefMut for DKIMHeader {
219 fn deref_mut(&mut self) -> &mut TaggedHeader {
220 &mut self.tagged
221 }
222}
223
224impl DKIMHeader {
225 pub fn parse(value: &str) -> Result<Self, DKIMError> {
227 let tagged = TaggedHeader::parse(value)?;
228 let header = DKIMHeader { tagged };
229
230 header.validate_required_tags()?;
231
232 if header.get_required_tag("v") != "1" {
234 return Err(DKIMError::IncompatibleVersion);
235 }
236
237 if let Some(user) = header.get_tag("i") {
240 let signing_domain = header.get_required_tag("d");
241 let Some((_local, domain)) = user.split_once('@') else {
242 return Err(DKIMError::DomainMismatch);
243 };
244
245 let i_domain = Name::from_str_relaxed(domain).map_err(|_| DKIMError::DomainMismatch)?;
246 let d_domain =
247 Name::from_str_relaxed(signing_domain).map_err(|_| DKIMError::DomainMismatch)?;
248
249 if !d_domain.zone_of(&i_domain) {
250 return Err(DKIMError::DomainMismatch);
251 }
252 }
253
254 header.check_common_tags()?;
255
256 Ok(header)
257 }
258
259 fn validate_required_tags(&self) -> Result<(), DKIMError> {
260 const REQUIRED_TAGS: &[&str] = &["v", "a", "b", "bh", "d", "h", "s"];
261 for required in REQUIRED_TAGS {
262 if self.get_tag(required).is_none() {
263 return Err(DKIMError::SignatureMissingRequiredTag(required));
264 }
265 }
266 Ok(())
267 }
268}
269
270#[derive(Clone)]
271pub(crate) struct TaggedHeaderBuilder {
272 header: TaggedHeader,
273 time: Option<chrono::DateTime<chrono::offset::Utc>>,
274}
275impl TaggedHeaderBuilder {
276 pub(crate) fn new() -> Self {
277 TaggedHeaderBuilder {
278 header: TaggedHeader::default(),
279 time: None,
280 }
281 }
282
283 pub(crate) fn add_tag(mut self, name: &str, value: &str) -> Self {
284 let tag = parser::Tag {
285 name: name.to_owned(),
286 value: value.to_owned(),
287 raw_value: value.to_owned(),
288 };
289 self.header.tags.insert(name.to_owned(), tag);
290
291 self
292 }
293
294 pub(crate) fn set_signed_headers(self, headers: &HeaderList) -> Self {
295 let value = headers.as_h_list();
296 self.add_tag("h", &value)
297 }
298
299 pub(crate) fn set_expiry(self, duration: chrono::Duration) -> Result<Self, DKIMError> {
300 let time = self.time.ok_or(DKIMError::BuilderError(
301 "TaggedHeaderBuilder: set_time must be called prior to calling set_expiry",
302 ))?;
303 let expiry = (time + duration).timestamp();
304 Ok(self.add_tag("x", &expiry.to_string()))
305 }
306
307 pub(crate) fn set_time(mut self, time: chrono::DateTime<chrono::offset::Utc>) -> Self {
308 self.time = Some(time);
309 self.add_tag("t", &time.timestamp().to_string())
310 }
311
312 pub(crate) fn build(mut self) -> TaggedHeader {
313 self.header.raw_bytes = self.header.serialize();
314 self.header
315 }
316}
317
318#[derive(Debug, Clone, Default)]
329pub struct ARCMessageSignatureHeader {
330 tagged: TaggedHeader,
331}
332
333impl std::ops::Deref for ARCMessageSignatureHeader {
334 type Target = TaggedHeader;
335 fn deref(&self) -> &TaggedHeader {
336 &self.tagged
337 }
338}
339impl std::ops::DerefMut for ARCMessageSignatureHeader {
340 fn deref_mut(&mut self) -> &mut TaggedHeader {
341 &mut self.tagged
342 }
343}
344
345impl ARCMessageSignatureHeader {
346 pub fn parse(value: &str) -> Result<Self, DKIMError> {
347 let tagged = TaggedHeader::parse(value)?;
348 let header = Self { tagged };
349
350 header.validate_required_tags()?;
351 header.check_common_tags()?;
352 header.arc_instance()?;
353
354 Ok(header)
355 }
356
357 fn validate_required_tags(&self) -> Result<(), DKIMError> {
358 const REQUIRED_TAGS: &[&str] = &["a", "b", "bh", "d", "h", "s", "i"];
359 for required in REQUIRED_TAGS {
360 if self.get_tag(required).is_none() {
361 return Err(DKIMError::SignatureMissingRequiredTag(required));
362 }
363 }
364 Ok(())
365 }
366}
367
368#[derive(Debug, Clone, Default)]
369pub struct ARCSealHeader {
370 tagged: TaggedHeader,
371}
372
373impl std::ops::Deref for ARCSealHeader {
374 type Target = TaggedHeader;
375 fn deref(&self) -> &TaggedHeader {
376 &self.tagged
377 }
378}
379impl std::ops::DerefMut for ARCSealHeader {
380 fn deref_mut(&mut self) -> &mut TaggedHeader {
381 &mut self.tagged
382 }
383}
384
385impl ARCSealHeader {
386 pub fn parse(value: &str) -> Result<Self, DKIMError> {
387 let tagged = TaggedHeader::parse(value)?;
388 let header = Self { tagged };
389
390 header.validate_required_tags()?;
391 header.arc_instance()?;
392
393 if header.get_tag("h").is_some() {
394 }
396
397 Ok(header)
398 }
399
400 fn validate_required_tags(&self) -> Result<(), DKIMError> {
401 const REQUIRED_TAGS: &[&str] = &["a", "b", "d", "s", "i", "cv"];
402 for required in REQUIRED_TAGS {
403 if self.get_tag(required).is_none() {
404 return Err(DKIMError::SignatureMissingRequiredTag(required));
405 }
406 }
407 Ok(())
408 }
409
410 pub async fn verify(
411 &self,
412 resolver: &dyn Resolver,
413 header_list: &Vec<&Header<'_>>,
414 ) -> Result<(), DKIMError> {
415 let public_keys = crate::public_key::retrieve_public_keys(
416 resolver,
417 self.get_required_tag("d"),
418 self.get_required_tag("s"),
419 )
420 .await?;
421
422 let hash_algo = parser::parse_hash_algo(self.get_required_tag("a"))?;
423
424 let computed_headers_hash = crate::hash::compute_headers_hash(
425 crate::canonicalization::Type::Relaxed,
426 &header_list,
427 hash_algo,
428 self,
429 ARC_SEAL_HEADER_NAME,
430 )?;
431
432 let signature = data_encoding::BASE64
433 .decode(self.get_required_tag("b").as_bytes())
434 .map_err(|err| {
435 DKIMError::SignatureSyntaxError(format!("failed to decode signature: {}", err))
436 })?;
437
438 let mut errors = vec![];
439 for public_key in public_keys {
440 match crate::verify_signature(hash_algo, &computed_headers_hash, &signature, public_key)
441 {
442 Ok(true) => return Ok(()),
443 Ok(false) => {}
444 Err(err) => {
445 errors.push(err);
446 }
447 }
448 }
449
450 if let Some(err) = errors.pop() {
451 return Err(err);
453 }
454
455 Err(DKIMError::SignatureDidNotVerify)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_dkim_header_builder() {
467 let header = TaggedHeaderBuilder::new()
468 .add_tag("v", "1")
469 .add_tag("a", "something")
470 .build();
471 k9::snapshot!(header.raw(), "v=1; a=something;");
472 }
473
474 fn signed_header_list(headers: &[&str]) -> HeaderList {
475 HeaderList::new(headers.iter().map(|h| h.to_lowercase()).collect())
476 }
477
478 #[test]
479 fn test_dkim_header_builder_signed_headers() {
480 let header = TaggedHeaderBuilder::new()
481 .add_tag("v", "2")
482 .set_signed_headers(&signed_header_list(&["header1", "header2", "header3"]))
483 .build();
484 k9::snapshot!(
485 header.raw(),
486 r#"
487v=2;\r
488\th=header1:header2:header3;
489"#
490 );
491 }
492
493 #[test]
494 fn test_dkim_header_builder_time() {
495 use chrono::TimeZone;
496
497 let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap();
498
499 let header = TaggedHeaderBuilder::new()
500 .set_time(time)
501 .set_expiry(chrono::Duration::try_hours(3).expect("3 hours ok"))
502 .unwrap()
503 .build();
504 k9::snapshot!(header.raw(), "t=1609459201; x=1609470001;");
505 }
506
507 #[test]
508 fn test_parse_ams() {
509 let sig = "i=1; a=rsa-sha256; c=relaxed/relaxed; d=
510 messagingengine.com; h=date:from:reply-to:to:message-id:subject
511 :mime-version:content-type:content-transfer-encoding; s=fm3; t=
512 1761717439; bh=+BM/Umiva3F0xjsh9a2BcwzO1nr0Ru6oGRmgkMy9T3M=; b=I
513 M7xjn2qSjOx5fDFvQY+pEPJ74+w3h/UOZUKvdAt7gRP8rAe9C+Tz72izVJyY82xw
514 7LT7CBXnwk2DQpg9erhq1yYept4M5CKWLXoQHHUJam8mV4RMUnHgTLVlColIVUtY
515 hNAomZdsGNiG1iRGX0C4y81zYANJ11TXKOTvfuMLhG2uDIa8768O5jBa4jlBtGHd
516 Dn/87/T/J+plO/ZPiSwWKa+ZttR6yjwm0fdpXf+4y8u0+I8iYSw2EN0vgWMYEEMp
517 R1xuhMKD+bSlx130Rz2/5jFsVgLS7CfbTKK5CtqS3hl6EaLw/REBZeCYCHltzRWF
518 wt38/NIzJ3ykCswwds2YQ==";
519 ARCMessageSignatureHeader::parse(sig).unwrap();
520 }
521
522 #[test]
523 fn test_parse_as() {
524 let seal = "i=1; a=rsa-sha256; cv=none; d=messagingengine.com; s=fm3; t=
525 1761717439; b=Q1E9HuR4H0paxIiz15H8P3tGfzDp0XmYKhvyzGsPEBHr2xg610
526 ZV1nU6gLWmUl693usMKVxWGrIXbSZb13ICRK0gp1MfVJSQ/4IGM0VD9P5d9Vv7aL
527 Q/lx/a8Ar1ks1yEHeBRuZ6Q5GdYur8rgYr7UoOTJGwOOPTJ4C2TWGoHHIRoVECJv
528 mMa6jpcJ6SE6iK/76elugk65BheumbQ1YEnbjitchUsLAwSXMuO+mhLYGtmvBhOn
529 v3ewYQvD2jZzl2W+O73A08dQ/oeODDPqt6Fpv3XK572cTYPHhzmSbsxh9Lp7Z9MV
530 x2TACmO51Adnp3C1CcEw8K9ajAgyjNMW4ELA==";
531 ARCSealHeader::parse(seal).unwrap();
532 }
533}