1use crate::{canonicalization, DKIMError, ParsedEmail, TaggedHeader};
2use data_encoding::BASE64;
3use mailparsing::Header;
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) struct HeaderList(Vec<String>);
122
123impl HeaderList {
124 pub fn as_h_list(&self) -> String {
125 self.0.join(":")
126 }
127
128 pub fn compute_over_signed(&self, email: &ParsedEmail) -> Self {
131 let unique_header_names = {
132 let mut n = self.0.clone();
135 n.sort();
136 n.dedup();
137 n
138 };
139
140 let email_headers = email.get_headers();
141
142 let mut result = vec![];
143 for name in unique_header_names {
144 for _ in email_headers.iter_named(&name) {
145 result.push(name.clone());
146 }
147 result.push(name);
148 }
149
150 Self(result)
151 }
152
153 pub fn new(list: Vec<String>) -> Self {
156 let normalized: Vec<String> = list.into_iter().map(|s| s.to_ascii_lowercase()).collect();
157 Self(normalized)
158 }
159
160 pub fn compute_concrete_header_list<'a>(&self, email: &'a ParsedEmail) -> Vec<&'a Header<'a>> {
170 let mut headers = vec![];
171 let email_headers = email.get_headers();
172 let num_headers = email_headers.len();
173
174 let mut last_index: HashMap<&String, usize> = HashMap::new();
177
178 'outer: for name in &self.0 {
179 let index = last_index.get(name).unwrap_or(&num_headers);
180 for (header_index, header) in email_headers
181 .iter()
182 .enumerate()
183 .rev()
184 .skip(num_headers - index)
185 {
186 if header.get_name().eq_ignore_ascii_case(name) {
187 headers.push(header);
188 last_index.insert(name, header_index);
189 continue 'outer;
190 }
191 }
192
193 last_index.insert(name, 0);
200 }
201 headers
202 }
203}
204
205pub(crate) fn compute_headers_hash<'a>(
206 canonicalization_type: canonicalization::Type,
207 header_list: &Vec<&Header>,
208 hash_algo: HashAlgo,
209 dkim_header: &TaggedHeader,
210 signature_header_name: &str,
211) -> Result<Vec<u8>, DKIMError> {
212 let mut input = Vec::new();
213 let mut hasher = HashImpl::from_algo(hash_algo);
214
215 for header in header_list {
216 canonicalization_type.canon_header_into(
217 header.get_name(),
218 header.get_raw_value().as_bytes(),
219 &mut input,
220 );
221 }
222
223 {
226 let sign = dkim_header.get_required_raw_tag("b");
227 let value = dkim_header.raw().replace(sign, "");
228 let mut canonicalized_value = vec![];
229 canonicalization_type.canon_header_into(
230 signature_header_name,
231 value.as_bytes(),
232 &mut canonicalized_value,
233 );
234
235 canonicalized_value.truncate(canonicalized_value.len() - 2);
237
238 input.extend_from_slice(&canonicalized_value);
239 }
240 tracing::debug!("headers to hash: {:?}", input);
241
242 hasher.hash(&input);
243 let hash = hasher.finalize_bytes();
244 Ok(hash)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::{DKIMHeader, DKIM_SIGNATURE_HEADER_NAME};
251
252 fn dkim_header() -> DKIMHeader {
253 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()
254 }
255
256 #[test]
257 fn test_compute_body_hash_simple() {
258 let email = r#"To: test@sauleau.com
259Subject: subject
260From: Sven Sauleau <sven@cloudflare.com>
261
262Hello Alice
263 "#
264 .replace("\n", "\r\n");
265 let email = ParsedEmail::parse(email).unwrap();
266
267 let canonicalization_type = canonicalization::Type::Simple;
268 let length = None;
269 let hash_algo = HashAlgo::RsaSha1;
270 assert_eq!(
271 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
272 "ya82MJvChLGBNSxeRvrSat5LliQ="
273 );
274 let hash_algo = HashAlgo::RsaSha256;
275 assert_eq!(
276 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
277 "KXQwQpX2zFwgixPbV6Dd18ZMJU04lLeRnwqzUp8uGwI=",
278 )
279 }
280
281 #[test]
282 fn test_compute_body_hash_relaxed() {
283 let email = r#"To: test@sauleau.com
284Subject: subject
285From: Sven Sauleau <sven@cloudflare.com>
286
287Hello Alice
288 "#
289 .replace("\n", "\r\n");
290 let email = ParsedEmail::parse(email).unwrap();
291
292 let canonicalization_type = canonicalization::Type::Relaxed;
293 let length = None;
294 let hash_algo = HashAlgo::RsaSha1;
295 assert_eq!(
296 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
297 "wpj48VhihzV7I31ZZZUp1UpTyyM="
298 );
299 let hash_algo = HashAlgo::RsaSha256;
300 assert_eq!(
301 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
302 "1bokzbYiRgXTKMQhrNhLJo1kjDDA1GILbpyTwyNa1uk=",
303 )
304 }
305
306 #[test]
307 fn test_compute_body_hash_length() {
308 let email = r#"To: test@sauleau.com
309Subject: subject
310From: Sven Sauleau <sven@cloudflare.com>
311
312Hello Alice
313 "#
314 .replace("\n", "\r\n");
315 let email = ParsedEmail::parse(email).unwrap();
316
317 let canonicalization_type = canonicalization::Type::Relaxed;
318 let length = Some(3);
319 let hash_algo = HashAlgo::RsaSha1;
320 assert_eq!(
321 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
322 "28LR/tDcN6cK6g83aVjIAu3cBVk="
323 );
324 let hash_algo = HashAlgo::RsaSha256;
325 assert_eq!(
326 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
327 "t4nCTc22jEQ3sEwYa/I5pyB+dXP7GyKnSf4ae42W0pI=",
328 )
329 }
330
331 #[test]
332 fn test_compute_body_hash_empty_simple() {
333 let email = ParsedEmail::parse("Subject: nothing\r\n\r\n").unwrap();
334
335 let canonicalization_type = canonicalization::Type::Simple;
336 let length = None;
337 let hash_algo = HashAlgo::RsaSha1;
338 assert_eq!(
339 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
340 "uoq1oCgLlTqpdDX/iUbLy7J1Wic="
341 );
342 let hash_algo = HashAlgo::RsaSha256;
343 assert_eq!(
344 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
345 "frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY="
346 )
347 }
348
349 #[test]
350 fn test_compute_body_hash_empty_relaxed() {
351 let email = ParsedEmail::parse("Subject: nothing\r\n\r\n").unwrap();
352
353 let canonicalization_type = canonicalization::Type::Relaxed;
354 let length = None;
355 let hash_algo = HashAlgo::RsaSha1;
356 assert_eq!(
357 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
358 "2jmj7l5rSw0yVb/vlWAYkK/YBwk="
359 );
360 let hash_algo = HashAlgo::RsaSha256;
361 assert_eq!(
362 compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
363 "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
364 )
365 }
366
367 #[test]
368 fn test_compute_headers_hash_simple() {
369 let email = r#"To: test@sauleau.com
370Subject: subject
371From: Sven Sauleau <sven@cloudflare.com>
372
373Hello Alice
374 "#
375 .replace("\n", "\r\n");
376 let email = ParsedEmail::parse(email).unwrap();
377
378 let canonicalization_type = canonicalization::Type::Simple;
379 let hash_algo = HashAlgo::RsaSha1;
380 let headers = HeaderList::new(vec!["To".to_owned(), "Subject".to_owned()])
381 .compute_concrete_header_list(&email);
382 assert_eq!(
383 compute_headers_hash(
384 canonicalization_type,
385 &headers,
386 hash_algo,
387 &dkim_header(),
388 DKIM_SIGNATURE_HEADER_NAME
389 )
390 .unwrap(),
391 &[
392 214, 155, 167, 0, 209, 70, 127, 126, 160, 53, 79, 106, 141, 240, 35, 121, 255, 190,
393 166, 229
394 ],
395 );
396 let hash_algo = HashAlgo::RsaSha256;
397 assert_eq!(
398 compute_headers_hash(
399 canonicalization_type,
400 &headers,
401 hash_algo,
402 &dkim_header(),
403 DKIM_SIGNATURE_HEADER_NAME
404 )
405 .unwrap(),
406 &[
407 76, 143, 13, 248, 17, 209, 243, 111, 40, 96, 160, 242, 116, 86, 37, 249, 134, 253,
408 196, 89, 6, 24, 157, 130, 142, 198, 27, 166, 127, 179, 72, 247
409 ]
410 )
411 }
412
413 #[test]
414 fn test_compute_headers_hash_relaxed() {
415 let email = r#"To: test@sauleau.com
416Subject: subject
417From: Sven Sauleau <sven@cloudflare.com>
418
419Hello Alice
420 "#
421 .replace("\n", "\r\n");
422 let email = ParsedEmail::parse(email).unwrap();
423
424 let canonicalization_type = canonicalization::Type::Relaxed;
425 let hash_algo = HashAlgo::RsaSha1;
426 let headers = HeaderList::new(vec!["To".to_owned(), "Subject".to_owned()])
427 .compute_concrete_header_list(&email);
428 assert_eq!(
429 compute_headers_hash(
430 canonicalization_type,
431 &headers,
432 hash_algo,
433 &dkim_header(),
434 DKIM_SIGNATURE_HEADER_NAME
435 )
436 .unwrap(),
437 &[
438 14, 171, 230, 1, 77, 117, 47, 207, 243, 167, 179, 5, 150, 82, 154, 25, 125, 124,
439 44, 164
440 ]
441 );
442 let hash_algo = HashAlgo::RsaSha256;
443 assert_eq!(
444 compute_headers_hash(
445 canonicalization_type,
446 &headers,
447 hash_algo,
448 &dkim_header(),
449 DKIM_SIGNATURE_HEADER_NAME
450 )
451 .unwrap(),
452 &[
453 45, 186, 211, 81, 49, 111, 18, 147, 180, 245, 207, 39, 9, 9, 118, 137, 248, 204,
454 70, 214, 16, 98, 216, 111, 230, 130, 196, 3, 60, 201, 166, 224
455 ]
456 )
457 }
458
459 #[test]
460 fn test_get_body() {
461 let email = ParsedEmail::parse("Subject: A\r\n\r\nContent\n.hi\n.hello..").unwrap();
462 assert_eq!(email.get_body(), "Content\n.hi\n.hello..");
463 }
464
465 fn select_headers<'a>(
466 header_list: &HeaderList,
467 email: &'a ParsedEmail,
468 ) -> Vec<(&'a str, &'a [u8])> {
469 header_list
470 .compute_concrete_header_list(email)
471 .into_iter()
472 .map(|header| (header.get_name(), header.get_raw_value().as_bytes()))
473 .collect()
474 }
475
476 #[test]
477 fn test_select_headers_unique() {
478 let header_list = HeaderList::new(vec![
479 "from".to_string(),
480 "subject".to_string(),
481 "to".to_string(),
482 ]);
483
484 let email1 =
485 ParsedEmail::parse("from: biz\r\nfoo: bar\r\nfrom: baz\r\nsubject: boring\r\n\r\ntest")
486 .unwrap();
487
488 let result1 = select_headers(&header_list, &email1);
489 assert_eq!(
490 result1,
491 vec![("from", &b"baz"[..]), ("subject", &b"boring"[..]),]
492 );
493
494 let email2 =
495 ParsedEmail::parse("From: biz\r\nFoo: bar\r\nSubject: Boring\r\n\r\ntest").unwrap();
496
497 let result2 = select_headers(&header_list, &email2);
498 assert_eq!(
499 result2,
500 vec![("From", &b"biz"[..]), ("Subject", &b"Boring"[..]),]
501 );
502 }
503
504 #[test]
505 fn test_select_headers_multiple() {
506 let header_list = HeaderList::new(vec![
507 "from".to_string(),
508 "subject".to_string(),
509 "to".to_string(),
510 "from".to_string(),
511 ]);
512
513 let email1 =
514 ParsedEmail::parse("from: biz\r\nfoo: bar\r\nfrom: baz\r\nsubject: boring\r\n\r\ntest")
515 .unwrap();
516
517 let result1 = select_headers(&header_list, &email1);
518 assert_eq!(
519 result1,
520 vec![
521 ("from", &b"baz"[..]),
522 ("subject", &b"boring"[..]),
523 ("from", &b"biz"[..]),
524 ]
525 );
526
527 let email2 =
528 ParsedEmail::parse("From: biz\r\nFoo: bar\r\nSubject: Boring\r\n\r\ntest").unwrap();
529
530 let result2 = select_headers(&header_list, &email2);
531 assert_eq!(
532 result2,
533 vec![("From", &b"biz"[..]), ("Subject", &b"Boring"[..]),]
534 );
535 }
536}