1use crate::header::HEADER;
2use crate::{canonicalization, DKIMError, DKIMHeader, ParsedEmail};
3use data_encoding::BASE64;
4use sha1::{Digest as _, Sha1};
5use sha2::Sha256;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy)]
9pub enum HashAlgo {
10 RsaSha1,
11 RsaSha256,
12 Ed25519Sha256,
13}
14
15impl HashAlgo {
16 pub fn algo_name(&self) -> &'static str {
17 match self {
18 Self::RsaSha1 => "rsa-sha1",
19 Self::RsaSha256 => "rsa-sha256",
20 Self::Ed25519Sha256 => "ed25519-sha256",
21 }
22 }
23}
24
25pub(crate) struct LimitHasher {
26 pub limit: usize,
27 pub hashed: usize,
28 pub hasher: HashImpl,
29}
30
31impl LimitHasher {
32 pub fn hash(&mut self, bytes: &[u8]) {
33 let remain = self.limit - self.hashed;
34 let len = bytes.len().min(remain);
35 self.hasher.hash(&bytes[..len]);
36 self.hashed += len;
37 }
38
39 pub fn finalize(self) -> String {
40 self.hasher.finalize()
41 }
42
43 #[cfg(test)]
44 pub fn finalize_bytes(self) -> Vec<u8> {
45 self.hasher.finalize_bytes()
46 }
47}
48
49pub(crate) enum HashImpl {
50 Sha1(Sha1),
51 Sha256(Sha256),
52 #[cfg(test)]
53 Copy(Vec<u8>),
54}
55
56impl HashImpl {
57 pub fn from_algo(algo: HashAlgo) -> Self {
58 match algo {
59 HashAlgo::RsaSha1 => Self::Sha1(Sha1::new()),
60 HashAlgo::RsaSha256 | HashAlgo::Ed25519Sha256 => Self::Sha256(Sha256::new()),
61 }
62 }
63
64 #[cfg(test)]
65 pub fn copy_data() -> Self {
66 Self::Copy(vec![])
67 }
68
69 pub fn hash(&mut self, bytes: &[u8]) {
70 match self {
71 Self::Sha1(hasher) => hasher.update(bytes),
72 Self::Sha256(hasher) => hasher.update(bytes),
73 #[cfg(test)]
74 Self::Copy(data) => data.extend_from_slice(bytes),
75 }
76 }
77
78 pub fn finalize(self) -> String {
79 match self {
80 Self::Sha1(hasher) => BASE64.encode(&hasher.finalize()),
81 Self::Sha256(hasher) => BASE64.encode(&hasher.finalize()),
82 #[cfg(test)]
83 Self::Copy(data) => String::from_utf8_lossy(&data).into(),
84 }
85 }
86
87 pub fn finalize_bytes(self) -> Vec<u8> {
88 match self {
89 Self::Sha1(hasher) => hasher.finalize().to_vec(),
90 Self::Sha256(hasher) => hasher.finalize().to_vec(),
91 #[cfg(test)]
92 Self::Copy(data) => data,
93 }
94 }
95}
96
97pub(crate) fn compute_body_hash<'a>(
100 canonicalization_type: canonicalization::Type,
101 length: Option<usize>,
102 hash_algo: HashAlgo,
103 email: &'a ParsedEmail<'a>,
104) -> Result<String, DKIMError> {
105 let body = email.get_body();
106 let limit = length.unwrap_or(usize::MAX);
107
108 let mut hasher = LimitHasher {
109 hasher: HashImpl::from_algo(hash_algo),
110 limit,
111 hashed: 0,
112 };
113
114 canonicalization_type.canon_body(body.as_bytes(), &mut hasher);
115
116 Ok(hasher.finalize())
117}
118
119#[derive(Debug)]
121pub(crate) enum HeaderList {
122 MaybeMultiple(Vec<String>),
124 Unique(Vec<String>),
126}
127
128impl HeaderList {
129 pub fn as_h_list(&self) -> String {
130 match self {
131 Self::MaybeMultiple(list) | Self::Unique(list) => list.join(":"),
132 }
133 }
134
135 pub fn compute_over_signed(&self, email: &ParsedEmail) -> Self {
138 let unique_header_names = match self {
139 Self::Unique(names) => names.clone(),
140 Self::MaybeMultiple(names) => {
141 let mut n = names.clone();
144 n.sort();
145 n.dedup();
146 n
147 }
148 };
149
150 let email_headers = email.get_headers();
151
152 let mut result = vec![];
153 for name in unique_header_names {
154 for _ in email_headers.iter_named(&name) {
155 result.push(name.clone());
156 }
157 result.push(name);
158 }
159
160 Self::MaybeMultiple(result)
161 }
162
163 pub fn new(list: Vec<String>) -> Self {
166 let normalized: Vec<String> = list.into_iter().map(|s| s.to_ascii_lowercase()).collect();
167
168 let mut all_single = true;
169 for name in &normalized {
170 let n: usize = normalized
171 .iter()
172 .map(|candidate| if candidate == name { 1 } else { 0 })
173 .sum();
174 if n > 1 {
175 all_single = false;
176 break;
177 }
178 }
179
180 if all_single {
181 Self::Unique(normalized)
182 } else {
183 Self::MaybeMultiple(normalized)
184 }
185 }
186
187 fn apply<'a, F: FnMut(&'a str, &'a [u8])>(&self, email: &'a ParsedEmail, apply: F) {
190 match self {
191 Self::MaybeMultiple(list) => Self::apply_multiple(list, email, apply),
192 Self::Unique(list) => Self::apply_unique(list, email, apply),
193 }
194 }
195
196 fn apply_unique<'a, F: FnMut(&'a str, &'a [u8])>(
200 header_list: &[String],
201 email: &'a ParsedEmail,
202 mut apply: F,
203 ) {
204 let email_headers = email.get_headers();
205
206 'outer: for name in header_list {
207 for header in email_headers.iter().rev() {
208 if header.get_name().eq_ignore_ascii_case(name) {
209 apply(header.get_name(), header.get_raw_value().as_bytes());
210 continue 'outer;
211 }
212 }
213 }
214 }
215
216 fn apply_multiple<'a, F: FnMut(&'a str, &'a [u8])>(
226 header_list: &[String],
227 email: &'a ParsedEmail,
228 mut apply: F,
229 ) {
230 let email_headers = email.get_headers();
231 let num_headers = email_headers.len();
232
233 let mut last_index: HashMap<&String, usize> = HashMap::new();
236
237 'outer: for name in header_list {
238 let index = last_index.get(name).unwrap_or(&num_headers);
239 for (header_index, header) in email_headers
240 .iter()
241 .enumerate()
242 .rev()
243 .skip(num_headers - index)
244 {
245 if header.get_name().eq_ignore_ascii_case(name) {
246 apply(header.get_name(), header.get_raw_value().as_bytes());
247 last_index.insert(name, header_index);
248 continue 'outer;
249 }
250 }
251
252 last_index.insert(name, 0);
258 }
259 }
260}
261
262pub(crate) fn compute_headers_hash<'a>(
263 canonicalization_type: canonicalization::Type,
264 headers: &HeaderList,
265 hash_algo: HashAlgo,
266 dkim_header: &DKIMHeader,
267 email: &'a ParsedEmail<'a>,
268) -> Result<Vec<u8>, DKIMError> {
269 let mut input = Vec::new();
270 let mut hasher = HashImpl::from_algo(hash_algo);
271
272 headers.apply(email, |key, value| {
273 canonicalization_type.canon_header_into(key, value, &mut input);
274 });
275
276 {
279 let sign = dkim_header.get_required_raw_tag("b");
280 let value = dkim_header.raw_bytes.replace(sign, "");
281 let mut canonicalized_value = vec![];
282 canonicalization_type.canon_header_into(HEADER, value.as_bytes(), &mut canonicalized_value);
283
284 canonicalized_value.truncate(canonicalized_value.len() - 2);
286
287 input.extend_from_slice(&canonicalized_value);
288 }
289 tracing::debug!("headers to hash: {:?}", input);
290
291 hasher.hash(&input);
292 let hash = hasher.finalize_bytes();
293 Ok(hash)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn dkim_header() -> DKIMHeader {
301 crate::DKIMHeader::parse("v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=smtp; d=test.com; t=1641506955; h=content-type:to: subject:date:from:mime-version:sender; bh=PU2XIErWsXvhvt1W96ntPWZ2VImjVZ3vBY2T/A+wA3A=; b=PIO0A014nyntOGKdTdtvCJor9ZxvP1M3hoLeEh8HqZ+RvAyEKdAc7VOg+/g/OTaZgsmw6U sZCoN0YNVp+2o9nkaeUslsVz3M4I55HcZnarxl+fhplIMcJ/3s0nIhXL51MfGPRqPbB7/M Gjg9/07/2vFoid6Kitg6Z+CfoD2wlSRa8xDfmeyA2cHpeVuGQhGxu7BXuU8kGbeM4+weit Ql3t9zalhikEPI5Pr7dzYFrgWNOEO6w6rQfG7niKON1BimjdbJlGanC7cO4UL361hhXT4X iXLnC9TG39xKFPT/+4nkHy8pp6YvWkD3wKlBjwkYNm0JvKGwTskCMDeTwxXhAg==").unwrap()
302 }
303
304 #[test]
305 fn test_compute_body_hash_simple() {
306 let email = r#"To: test@sauleau.com
307Subject: subject
308From: Sven Sauleau <sven@cloudflare.com>
309
310Hello Alice
311 "#
312 .replace("\n", "\r\n");
313 let email = ParsedEmail::parse(email).unwrap();
314
315 let canonicalization_type = canonicalization::Type::Simple;
316 let length = None;
317 let hash_algo = HashAlgo::RsaSha1;
318 assert_eq!(
319 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
320 "ya82MJvChLGBNSxeRvrSat5LliQ="
321 );
322 let hash_algo = HashAlgo::RsaSha256;
323 assert_eq!(
324 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
325 "KXQwQpX2zFwgixPbV6Dd18ZMJU04lLeRnwqzUp8uGwI=",
326 )
327 }
328
329 #[test]
330 fn test_compute_body_hash_relaxed() {
331 let email = r#"To: test@sauleau.com
332Subject: subject
333From: Sven Sauleau <sven@cloudflare.com>
334
335Hello Alice
336 "#
337 .replace("\n", "\r\n");
338 let email = ParsedEmail::parse(email).unwrap();
339
340 let canonicalization_type = canonicalization::Type::Relaxed;
341 let length = None;
342 let hash_algo = HashAlgo::RsaSha1;
343 assert_eq!(
344 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
345 "wpj48VhihzV7I31ZZZUp1UpTyyM="
346 );
347 let hash_algo = HashAlgo::RsaSha256;
348 assert_eq!(
349 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
350 "1bokzbYiRgXTKMQhrNhLJo1kjDDA1GILbpyTwyNa1uk=",
351 )
352 }
353
354 #[test]
355 fn test_compute_body_hash_length() {
356 let email = r#"To: test@sauleau.com
357Subject: subject
358From: Sven Sauleau <sven@cloudflare.com>
359
360Hello Alice
361 "#
362 .replace("\n", "\r\n");
363 let email = ParsedEmail::parse(email).unwrap();
364
365 let canonicalization_type = canonicalization::Type::Relaxed;
366 let length = Some(3);
367 let hash_algo = HashAlgo::RsaSha1;
368 assert_eq!(
369 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
370 "28LR/tDcN6cK6g83aVjIAu3cBVk="
371 );
372 let hash_algo = HashAlgo::RsaSha256;
373 assert_eq!(
374 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
375 "t4nCTc22jEQ3sEwYa/I5pyB+dXP7GyKnSf4ae42W0pI=",
376 )
377 }
378
379 #[test]
380 fn test_compute_body_hash_empty_simple() {
381 let email = ParsedEmail::parse("Subject: nothing\r\n\r\n").unwrap();
382
383 let canonicalization_type = canonicalization::Type::Simple;
384 let length = None;
385 let hash_algo = HashAlgo::RsaSha1;
386 assert_eq!(
387 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
388 "uoq1oCgLlTqpdDX/iUbLy7J1Wic="
389 );
390 let hash_algo = HashAlgo::RsaSha256;
391 assert_eq!(
392 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
393 "frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY="
394 )
395 }
396
397 #[test]
398 fn test_compute_body_hash_empty_relaxed() {
399 let email = ParsedEmail::parse("Subject: nothing\r\n\r\n").unwrap();
400
401 let canonicalization_type = canonicalization::Type::Relaxed;
402 let length = None;
403 let hash_algo = HashAlgo::RsaSha1;
404 assert_eq!(
405 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
406 "2jmj7l5rSw0yVb/vlWAYkK/YBwk="
407 );
408 let hash_algo = HashAlgo::RsaSha256;
409 assert_eq!(
410 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
411 "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
412 )
413 }
414
415 #[test]
416 fn test_compute_headers_hash_simple() {
417 let email = r#"To: test@sauleau.com
418Subject: subject
419From: Sven Sauleau <sven@cloudflare.com>
420
421Hello Alice
422 "#
423 .replace("\n", "\r\n");
424 let email = ParsedEmail::parse(email).unwrap();
425
426 let canonicalization_type = canonicalization::Type::Simple;
427 let hash_algo = HashAlgo::RsaSha1;
428 let headers = HeaderList::new(vec!["To".to_owned(), "Subject".to_owned()]);
429 assert_eq!(
430 compute_headers_hash(
431 canonicalization_type,
432 &headers,
433 hash_algo,
434 &dkim_header(),
435 &email
436 )
437 .unwrap(),
438 &[
439 214, 155, 167, 0, 209, 70, 127, 126, 160, 53, 79, 106, 141, 240, 35, 121, 255, 190,
440 166, 229
441 ],
442 );
443 let hash_algo = HashAlgo::RsaSha256;
444 assert_eq!(
445 compute_headers_hash(
446 canonicalization_type,
447 &headers,
448 hash_algo,
449 &dkim_header(),
450 &email
451 )
452 .unwrap(),
453 &[
454 76, 143, 13, 248, 17, 209, 243, 111, 40, 96, 160, 242, 116, 86, 37, 249, 134, 253,
455 196, 89, 6, 24, 157, 130, 142, 198, 27, 166, 127, 179, 72, 247
456 ]
457 )
458 }
459
460 #[test]
461 fn test_compute_headers_hash_relaxed() {
462 let email = r#"To: test@sauleau.com
463Subject: subject
464From: Sven Sauleau <sven@cloudflare.com>
465
466Hello Alice
467 "#
468 .replace("\n", "\r\n");
469 let email = ParsedEmail::parse(email).unwrap();
470
471 let canonicalization_type = canonicalization::Type::Relaxed;
472 let hash_algo = HashAlgo::RsaSha1;
473 let headers = HeaderList::new(vec!["To".to_owned(), "Subject".to_owned()]);
474 assert_eq!(
475 compute_headers_hash(
476 canonicalization_type,
477 &headers,
478 hash_algo,
479 &dkim_header(),
480 &email
481 )
482 .unwrap(),
483 &[
484 14, 171, 230, 1, 77, 117, 47, 207, 243, 167, 179, 5, 150, 82, 154, 25, 125, 124,
485 44, 164
486 ]
487 );
488 let hash_algo = HashAlgo::RsaSha256;
489 assert_eq!(
490 compute_headers_hash(
491 canonicalization_type,
492 &headers,
493 hash_algo,
494 &dkim_header(),
495 &email
496 )
497 .unwrap(),
498 &[
499 45, 186, 211, 81, 49, 111, 18, 147, 180, 245, 207, 39, 9, 9, 118, 137, 248, 204,
500 70, 214, 16, 98, 216, 111, 230, 130, 196, 3, 60, 201, 166, 224
501 ]
502 )
503 }
504
505 #[test]
506 fn test_get_body() {
507 let email = ParsedEmail::parse("Subject: A\r\n\r\nContent\n.hi\n.hello..").unwrap();
508 assert_eq!(email.get_body(), "Content\n.hi\n.hello..");
509 }
510
511 fn select_headers<'a>(
512 header_list: &HeaderList,
513 email: &'a ParsedEmail,
514 ) -> Vec<(&'a str, &'a [u8])> {
515 let mut result = vec![];
516 header_list.apply(email, |key, value| {
517 result.push((key, value));
518 });
519 result
520 }
521
522 #[test]
523 fn test_select_headers_unique() {
524 let header_list = HeaderList::new(vec![
525 "from".to_string(),
526 "subject".to_string(),
527 "to".to_string(),
528 ]);
529
530 let email1 =
531 ParsedEmail::parse("from: biz\r\nfoo: bar\r\nfrom: baz\r\nsubject: boring\r\n\r\ntest")
532 .unwrap();
533
534 let result1 = select_headers(&header_list, &email1);
535 assert_eq!(
536 result1,
537 vec![("from", &b"baz"[..]), ("subject", &b"boring"[..]),]
538 );
539
540 let email2 =
541 ParsedEmail::parse("From: biz\r\nFoo: bar\r\nSubject: Boring\r\n\r\ntest").unwrap();
542
543 let result2 = select_headers(&header_list, &email2);
544 assert_eq!(
545 result2,
546 vec![("From", &b"biz"[..]), ("Subject", &b"Boring"[..]),]
547 );
548 }
549
550 #[test]
551 fn test_select_headers_multiple() {
552 let header_list = HeaderList::new(vec![
553 "from".to_string(),
554 "subject".to_string(),
555 "to".to_string(),
556 "from".to_string(),
557 ]);
558
559 let email1 =
560 ParsedEmail::parse("from: biz\r\nfoo: bar\r\nfrom: baz\r\nsubject: boring\r\n\r\ntest")
561 .unwrap();
562
563 let result1 = select_headers(&header_list, &email1);
564 assert_eq!(
565 result1,
566 vec![
567 ("from", &b"baz"[..]),
568 ("subject", &b"boring"[..]),
569 ("from", &b"biz"[..]),
570 ]
571 );
572
573 let email2 =
574 ParsedEmail::parse("From: biz\r\nFoo: bar\r\nSubject: Boring\r\n\r\ntest").unwrap();
575
576 let result2 = select_headers(&header_list, &email2);
577 assert_eq!(
578 result2,
579 vec![("From", &b"biz"[..]), ("Subject", &b"Boring"[..]),]
580 );
581 }
582}