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