mail_auth/dkim/
canonicalize.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use crate::common::headers::{HeaderStream, Writable, Writer};
8
9use super::{Canonicalization, Signature};
10
11pub struct CanonicalBody<'a> {
12    canonicalization: Canonicalization,
13    body: &'a [u8],
14}
15
16impl Writable for CanonicalBody<'_> {
17    fn write(self, hasher: &mut impl Writer) {
18        let mut crlf_seq = 0;
19
20        match self.canonicalization {
21            Canonicalization::Relaxed => {
22                let mut last_ch = 0;
23                let mut is_empty = true;
24
25                for &ch in self.body {
26                    match ch {
27                        b' ' | b'\t' => {
28                            while crlf_seq > 0 {
29                                hasher.write(b"\r\n");
30                                crlf_seq -= 1;
31                            }
32                            is_empty = false;
33                        }
34                        b'\n' => {
35                            crlf_seq += 1;
36                        }
37                        b'\r' => {}
38                        _ => {
39                            while crlf_seq > 0 {
40                                hasher.write(b"\r\n");
41                                crlf_seq -= 1;
42                            }
43
44                            if last_ch == b' ' || last_ch == b'\t' {
45                                hasher.write(b" ");
46                            }
47
48                            hasher.write(&[ch]);
49                            is_empty = false;
50                        }
51                    }
52
53                    last_ch = ch;
54                }
55
56                if !is_empty {
57                    hasher.write(b"\r\n");
58                }
59            }
60            Canonicalization::Simple => {
61                for &ch in self.body {
62                    match ch {
63                        b'\n' => {
64                            crlf_seq += 1;
65                        }
66                        b'\r' => {}
67                        _ => {
68                            while crlf_seq > 0 {
69                                hasher.write(b"\r\n");
70                                crlf_seq -= 1;
71                            }
72                            hasher.write(&[ch]);
73                        }
74                    }
75                }
76
77                hasher.write(b"\r\n");
78            }
79        }
80    }
81}
82
83impl Canonicalization {
84    pub fn canonicalize_headers<'a>(
85        &self,
86        headers: impl Iterator<Item = (&'a [u8], &'a [u8])>,
87        hasher: &mut impl Writer,
88    ) {
89        match self {
90            Canonicalization::Relaxed => {
91                for (name, value) in headers {
92                    for &ch in name {
93                        if !ch.is_ascii_whitespace() {
94                            hasher.write(&[ch.to_ascii_lowercase()]);
95                        }
96                    }
97
98                    hasher.write(b":");
99                    let mut bw = 0;
100                    let mut last_ch = 0;
101
102                    for &ch in value {
103                        if !ch.is_ascii_whitespace() {
104                            if [b' ', b'\t'].contains(&last_ch) && bw > 0 {
105                                hasher.write_len(b" ", &mut bw);
106                            }
107                            hasher.write_len(&[ch], &mut bw);
108                        }
109                        last_ch = ch;
110                    }
111
112                    if last_ch == b'\n' {
113                        hasher.write(b"\r\n");
114                    }
115                }
116            }
117            Canonicalization::Simple => {
118                for (name, value) in headers {
119                    hasher.write(name);
120                    hasher.write(b":");
121                    hasher.write(value);
122                }
123            }
124        }
125    }
126
127    pub fn canonical_headers<'a>(
128        &self,
129        headers: Vec<(&'a [u8], &'a [u8])>,
130    ) -> CanonicalHeaders<'a> {
131        CanonicalHeaders {
132            canonicalization: *self,
133            headers,
134        }
135    }
136
137    pub fn canonical_body<'a>(&self, body: &'a [u8], l: u64) -> CanonicalBody<'a> {
138        CanonicalBody {
139            canonicalization: *self,
140            body: if l == 0 || body.is_empty() {
141                body
142            } else {
143                &body[..std::cmp::min(l as usize, body.len())]
144            },
145        }
146    }
147
148    pub fn serialize_name(&self, writer: &mut impl Writer) {
149        writer.write(match self {
150            Canonicalization::Relaxed => b"relaxed",
151            Canonicalization::Simple => b"simple",
152        });
153    }
154}
155
156impl Signature {
157    pub fn canonicalize<'x>(
158        &self,
159        mut message: impl HeaderStream<'x>,
160    ) -> (usize, CanonicalHeaders<'x>, Vec<String>, CanonicalBody<'x>) {
161        let mut headers = Vec::with_capacity(self.h.len());
162        let mut found_headers = vec![false; self.h.len()];
163        let mut signed_headers = Vec::with_capacity(self.h.len());
164
165        while let Some((name, value)) = message.next_header() {
166            if let Some(pos) = self
167                .h
168                .iter()
169                .position(|header| name.eq_ignore_ascii_case(header.as_bytes()))
170            {
171                headers.push((name, value));
172                found_headers[pos] = true;
173                signed_headers.push(std::str::from_utf8(name).unwrap().into());
174            }
175        }
176
177        let body = message.body();
178        let body_len = body.len();
179        let canonical_headers = self.ch.canonical_headers(headers);
180        let canonical_body = self.ch.canonical_body(body, u64::MAX);
181
182        // Add any missing headers
183        signed_headers.reverse();
184        for (header, found) in self.h.iter().zip(found_headers) {
185            if !found {
186                signed_headers.push(header.to_string());
187            }
188        }
189
190        (body_len, canonical_headers, signed_headers, canonical_body)
191    }
192}
193
194pub struct CanonicalHeaders<'a> {
195    canonicalization: Canonicalization,
196    headers: Vec<(&'a [u8], &'a [u8])>,
197}
198
199impl Writable for CanonicalHeaders<'_> {
200    fn write(self, writer: &mut impl Writer) {
201        self.canonicalization
202            .canonicalize_headers(self.headers.into_iter().rev(), writer)
203    }
204}
205
206#[cfg(test)]
207mod test {
208    use mail_builder::encoders::base64::base64_encode;
209
210    use super::{CanonicalBody, CanonicalHeaders};
211    use crate::{
212        common::{
213            crypto::{HashImpl, Sha256},
214            headers::{HeaderIterator, Writable},
215        },
216        dkim::Canonicalization,
217    };
218
219    #[test]
220    #[allow(clippy::needless_collect)]
221    fn dkim_canonicalize() {
222        for (message, (relaxed_headers, relaxed_body), (simple_headers, simple_body)) in [
223            (
224                concat!(
225                    "A: X\r\n",
226                    "B : Y\t\r\n",
227                    "\tZ  \r\n",
228                    "\r\n",
229                    " C \r\n",
230                    "D \t E\r\n"
231                ),
232                (
233                    concat!("a:X\r\n", "b:Y Z\r\n",),
234                    concat!(" C\r\n", "D E\r\n"),
235                ),
236                ("A: X\r\nB : Y\t\r\n\tZ  \r\n", " C \r\nD \t E\r\n"),
237            ),
238            (
239                concat!(
240                    "  From : John\tdoe <jdoe@domain.com>\t\r\n",
241                    "SUB JECT:\ttest  \t  \r\n\r\n",
242                    " body \t   \r\n",
243                    "\r\n",
244                    "\r\n",
245                ),
246                (
247                    concat!("from:John doe <jdoe@domain.com>\r\n", "subject:test\r\n"),
248                    " body\r\n",
249                ),
250                (
251                    concat!(
252                        "  From : John\tdoe <jdoe@domain.com>\t\r\n",
253                        "SUB JECT:\ttest  \t  \r\n"
254                    ),
255                    " body \t   \r\n",
256                ),
257            ),
258            (
259                "H: value\t\r\n\r\n",
260                ("h:value\r\n", ""),
261                ("H: value\t\r\n", "\r\n"),
262            ),
263            (
264                "\tx\t: \t\t\tz\r\n\r\nabc",
265                ("x:z\r\n", "abc\r\n"),
266                ("\tx\t: \t\t\tz\r\n", "abc\r\n"),
267            ),
268            (
269                "Subject: hello\r\n\r\n\r\n",
270                ("subject:hello\r\n", ""),
271                ("Subject: hello\r\n", "\r\n"),
272            ),
273        ] {
274            let mut header_iterator = HeaderIterator::new(message.as_bytes());
275            let parsed_headers = (&mut header_iterator).collect::<Vec<_>>();
276            let raw_body = header_iterator
277                .body_offset()
278                .map(|pos| &message.as_bytes()[pos..])
279                .unwrap_or_default();
280
281            for (canonicalization, expected_headers, expected_body) in [
282                (Canonicalization::Relaxed, relaxed_headers, relaxed_body),
283                (Canonicalization::Simple, simple_headers, simple_body),
284            ] {
285                let mut headers = Vec::new();
286                CanonicalHeaders {
287                    canonicalization,
288                    headers: parsed_headers.iter().cloned().rev().collect(),
289                }
290                .write(&mut headers);
291                assert_eq!(expected_headers, String::from_utf8(headers).unwrap());
292
293                let mut body = Vec::new();
294                CanonicalBody {
295                    canonicalization,
296                    body: raw_body,
297                }
298                .write(&mut body);
299                assert_eq!(expected_body, String::from_utf8(body).unwrap());
300            }
301        }
302
303        // Test empty body hashes
304        for (canonicalization, hash) in [
305            (
306                Canonicalization::Relaxed,
307                "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
308            ),
309            (
310                Canonicalization::Simple,
311                "frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=",
312            ),
313        ] {
314            for body in ["\r\n", ""] {
315                let mut hasher = Sha256::hasher();
316                CanonicalBody {
317                    canonicalization,
318                    body: body.as_bytes(),
319                }
320                .write(&mut hasher);
321
322                #[cfg(feature = "sha1")]
323                {
324                    use sha1::Digest;
325                    assert_eq!(
326                        String::from_utf8(base64_encode(hasher.finalize().as_ref()).unwrap())
327                            .unwrap(),
328                        hash,
329                    );
330                }
331
332                #[cfg(all(feature = "ring", not(feature = "sha1")))]
333                assert_eq!(
334                    String::from_utf8(base64_encode(hasher.finish().as_ref()).unwrap()).unwrap(),
335                    hash,
336                );
337            }
338        }
339    }
340}