kumo_dkim/
hash.rs

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
97/// Returns the hash of message's body
98/// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
99pub(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/// Holds a list of header names, normalized to lower case
120#[derive(Debug)]
121pub(crate) enum HeaderList {
122    /// A list of possibly duplicated header names
123    MaybeMultiple(Vec<String>),
124    /// A list of of unique header names
125    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    /// Computes the header list that should be used to over-sign
136    /// the provided email message, and returns it
137    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                // Note that Self::new() normalized the names, so we can
142                // simply dedup without further concern for normalization
143                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    /// Build a header list.
164    /// Analyzes the list to determine whether it is a unique list or not
165    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    /// Apply `apply` to each header in the provided email that
188    /// matches the headers, follow the order set out in Section 5.4.2
189    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    /// Perform the apply when we know that the list of header names
197    /// are unique.
198    /// We can avoid allocating any additional state for this case.
199    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    /// Section 5.4.2:
217    /// Signers wishing to sign multiple instances of such a header field MUST
218    /// include the header field name multiple times in the "h=" tag of the
219    /// DKIM-Signature header field and MUST sign such header fields in order
220    /// from the bottom of the header field block to the top.
221    ///
222    /// To facilitate this, we need to maintain state for each header name
223    /// in the list to ensure that we select the appropriate header in the
224    /// appropriate order.
225    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        // Note: this map only works correctly if the header names are normalized
234        // to lower case.  That happens in our constructor.
235        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            // When computing the signature, the nonexisting header field MUST be
253            // treated as the null string (including the header field name, header
254            // field value, all punctuation, and the trailing CRLF).
255            // -> don't include it in the returned signed_headers.
256
257            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    // Add the DKIM-Signature header in the hash. Remove the value of the
277    // signature (b) first.
278    {
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        // remove trailing "\r\n"
285        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}