kumo_dkim/
canonicalization.rs

1use crate::hash::LimitHasher;
2use bstr::ByteSlice;
3use memchr::memmem::Finder;
4use std::sync::LazyLock;
5
6#[derive(PartialEq, Clone, Debug, Copy)]
7pub enum Type {
8    Simple,
9    Relaxed,
10}
11
12impl Type {
13    pub fn canon_name(&self) -> &'static str {
14        match self {
15            Self::Simple => "simple",
16            Self::Relaxed => "relaxed",
17        }
18    }
19
20    pub(crate) fn canon_body(&self, body: &[u8], hasher: &mut LimitHasher) {
21        match self {
22            Self::Simple => body_simple(body, hasher),
23            Self::Relaxed => body_relaxed(body, hasher),
24        }
25    }
26
27    pub(crate) fn canon_header_into(&self, key: &[u8], value: &[u8], out: &mut Vec<u8>) {
28        match self {
29            Self::Simple => canonicalize_header_simple(key, value, out),
30            Self::Relaxed => canonicalize_header_relaxed(key, value, out),
31        }
32    }
33}
34
35fn do_body_simple(mut body: &[u8]) -> &[u8] {
36    if body.is_empty() {
37        return b"\r\n";
38    }
39
40    while body.ends_with(b"\r\n\r\n") {
41        body = &body[..body.len() - 2];
42    }
43
44    body
45}
46
47/// Canonicalize body using the simple canonicalization algorithm.
48fn body_simple(body: &[u8], hasher: &mut LimitHasher) {
49    let body = do_body_simple(body);
50    hasher.hash(body);
51}
52
53/// Helper for iterating lines using memmem
54struct IterLines<'haystack> {
55    haystack: &'haystack [u8],
56    inner: memchr::memmem::FindIter<'haystack, 'static>,
57    start: usize,
58    done: bool,
59}
60
61impl<'haystack> Iterator for IterLines<'haystack> {
62    type Item = &'haystack [u8];
63
64    fn next(&mut self) -> Option<Self::Item> {
65        if self.done {
66            return None;
67        }
68
69        match self.inner.next() {
70            Some(idx) => {
71                let line = &self.haystack[self.start..idx + 2];
72                self.start = idx + 2;
73                Some(line)
74            }
75            None => {
76                self.done = true;
77                let line = &self.haystack[self.start..];
78                if line.is_empty() {
79                    None
80                } else {
81                    Some(line)
82                }
83            }
84        }
85    }
86}
87
88fn iter_lines(haystack: &'_ [u8]) -> IterLines<'_> {
89    static CRLF: LazyLock<Finder> = LazyLock::new(|| memchr::memmem::Finder::new("\r\n"));
90    IterLines {
91        haystack,
92        inner: CRLF.find_iter(haystack),
93        start: 0,
94        done: false,
95    }
96}
97
98/// Canonicalize a body using the relaxed canonicalization algorithm from
99/// RFC 6376 section 3.4.4.
100/// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.4
101fn body_relaxed(body: &[u8], hasher: &mut LimitHasher) {
102    let mut pending_empty_lines = 0usize;
103
104    for mut line in iter_lines(body) {
105        // Ignore all whitespace at the end of the line
106        line = trim_ws_end(line);
107
108        // Empty lines are only emitted if followed by a non-empty line. This
109        // drops all trailing empty lines, including a body consisting solely
110        // of CRLF, as required by RFC 6376 section 3.4.4.
111        if line.is_empty() {
112            pending_empty_lines += 1;
113            continue;
114        }
115
116        for _ in 0..pending_empty_lines {
117            hasher.hash(b"\r\n");
118        }
119        pending_empty_lines = 0;
120
121        let mut prior = 0;
122        // Reduce all sequences of WSP within a line to a single SP character.
123        for idx in memchr::memchr2_iter(b' ', b'\t', line) {
124            if prior > 0 && idx == prior {
125                // Part of a run; ignore this one
126                prior = idx + 1;
127                continue;
128            }
129
130            // Found a new run of space(s).
131            // Emit the bytes ahead of this one
132            hasher.hash(&line[prior..idx]);
133            // and emit the canonical space
134            hasher.hash(b" ");
135
136            prior = idx + 1;
137        }
138        // and emit the remainder
139        hasher.hash(&line[prior..]);
140
141        // and canonical newline
142        hasher.hash(b"\r\n");
143    }
144}
145
146// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.1
147fn canonicalize_header_simple(key: &[u8], value: &[u8], out: &mut Vec<u8>) {
148    out.extend_from_slice(key);
149    out.extend_from_slice(b": ");
150    out.extend_from_slice(value);
151    out.extend_from_slice(b"\r\n");
152}
153
154// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.2
155fn canonicalize_header_relaxed(key: &[u8], value: &[u8], out: &mut Vec<u8>) {
156    let key = key.to_ascii_lowercase();
157    let key = key.trim_end();
158
159    out.extend_from_slice(key.as_bytes());
160    out.extend_from_slice(b":");
161
162    let value = trim_ws_start(trim_ws_end(value));
163    let mut space_run = false;
164    for &c in value {
165        match c {
166            b'\r' | b'\n' => {}
167            b' ' | b'\t' => {
168                if space_run {
169                    continue;
170                }
171                space_run = true;
172                out.push(b' ');
173            }
174            _ => {
175                space_run = false;
176                out.push(c);
177            }
178        }
179    }
180
181    out.extend_from_slice(b"\r\n");
182}
183
184fn trim_ws_start(mut line: &[u8]) -> &[u8] {
185    while let Some(c) = line.first() {
186        match c {
187            b' ' | b'\t' | b'\r' | b'\n' => line = &line[1..],
188            _ => break,
189        }
190    }
191    line
192}
193
194fn trim_ws_end(mut line: &[u8]) -> &[u8] {
195    while let Some(c) = line.last() {
196        match c {
197            b' ' | b'\t' | b'\r' | b'\n' => {
198                line = &line[0..line.len() - 1];
199            }
200            _ => break,
201        }
202    }
203    line
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    fn header_relaxed(key: &str, value: &[u8]) -> Vec<u8> {
211        let mut result = vec![];
212        canonicalize_header_relaxed(key.as_bytes(), value, &mut result);
213        result
214    }
215
216    #[test]
217    fn test_canonicalize_header_relaxed() {
218        assert_eq!(header_relaxed("SUBJect", b" AbC\r\n"), b"subject:AbC\r\n");
219        assert_eq!(
220            header_relaxed("Subject \t", b"\t Your Name\t \r\n"),
221            b"subject:Your Name\r\n"
222        );
223        assert_eq!(
224            header_relaxed("Subject \t", b"\t Kimi \t \r\n No \t\r\n Na Wa\r\n"),
225            b"subject:Kimi No Na Wa\r\n"
226        );
227    }
228
229    fn body_relaxed(data: &[u8]) -> Vec<u8> {
230        body_relaxed_with_limit(data, usize::MAX)
231    }
232
233    fn body_relaxed_with_limit(data: &[u8], limit: usize) -> Vec<u8> {
234        let mut hasher = LimitHasher {
235            hasher: crate::hash::HashImpl::copy_data(),
236            limit,
237            hashed: 0,
238        };
239        super::body_relaxed(data, &mut hasher);
240        hasher.finalize_bytes()
241    }
242
243    /// A deliberately non-streaming implementation of RFC 6376 section
244    /// 3.4.4 for checking the production streaming implementation.
245    fn reference_body_relaxed(body: &[u8]) -> Vec<u8> {
246        let mut lines = vec![];
247        let mut start = 0;
248
249        while let Some(offset) = body[start..]
250            .windows(2)
251            .position(|window| window == b"\r\n")
252        {
253            let end = start + offset;
254            lines.push(&body[start..end]);
255            start = end + 2;
256        }
257
258        if start < body.len() {
259            lines.push(&body[start..]);
260        }
261
262        let mut canonical_lines = lines
263            .into_iter()
264            .map(|line| {
265                let mut canonical = vec![];
266                let mut pending_wsp = false;
267
268                for &byte in line {
269                    match byte {
270                        b' ' | b'\t' => pending_wsp = true,
271                        _ => {
272                            if pending_wsp {
273                                canonical.push(b' ');
274                                pending_wsp = false;
275                            }
276                            canonical.push(byte);
277                        }
278                    }
279                }
280
281                canonical
282            })
283            .collect::<Vec<_>>();
284
285        while canonical_lines.last().is_some_and(Vec::is_empty) {
286            canonical_lines.pop();
287        }
288
289        let mut canonical = canonical_lines
290            .into_iter()
291            .collect::<Vec<_>>()
292            .join(b"\r\n".as_slice());
293        if !canonical.is_empty() {
294            canonical.extend_from_slice(b"\r\n");
295        }
296        canonical
297    }
298
299    fn body_simple(data: &[u8]) -> Vec<u8> {
300        let mut hasher = LimitHasher {
301            hasher: crate::hash::HashImpl::copy_data(),
302            limit: usize::MAX,
303            hashed: 0,
304        };
305        super::body_simple(data, &mut hasher);
306        hasher.finalize_bytes()
307    }
308
309    #[test]
310    fn test_canonicalize_body_relaxed() {
311        assert_eq!(body_relaxed(b""), b"");
312        assert_eq!(body_relaxed(b"\r\n"), b"");
313        assert_eq!(body_relaxed(b"\r\n\r\n"), b"");
314        assert_eq!(body_relaxed(b" \t\r\n\t\r\n"), b"");
315        assert_eq!(body_relaxed(b"hey        \r\n"), b"hey\r\n");
316        assert_eq!(body_relaxed(b" C \r\nD \t E\r\n\r\n\r\n"), b" C\r\nD E\r\n");
317        assert_eq!(body_relaxed(b"\r\nhey \t\r\n \t\r\n"), b"\r\nhey\r\n");
318        assert_eq!(
319            body_relaxed(b"hey\r\n \t\r\nthere"),
320            b"hey\r\n\r\nthere\r\n"
321        );
322    }
323
324    #[test]
325    fn test_canonicalize_body_relaxed_against_reference() {
326        let line_variants: &[&[u8]] = &[
327            b"",
328            b"a",
329            b" ",
330            b"\t",
331            b" \t",
332            b"a ",
333            b" a",
334            b"a \t b",
335            b" a\t \tb ",
336        ];
337
338        for line_count in 0..=4u32 {
339            let combination_count = line_variants.len().pow(line_count);
340
341            for combination in 0..combination_count {
342                for terminated in [false, true] {
343                    let mut selected = combination;
344                    let mut body = vec![];
345
346                    for line_index in 0..line_count {
347                        if line_index > 0 {
348                            body.extend_from_slice(b"\r\n");
349                        }
350                        body.extend_from_slice(line_variants[selected % line_variants.len()]);
351                        selected /= line_variants.len();
352                    }
353
354                    if terminated {
355                        body.extend_from_slice(b"\r\n");
356                    }
357
358                    assert_eq!(
359                        body_relaxed(&body),
360                        reference_body_relaxed(&body),
361                        "body={body:?}"
362                    );
363                }
364            }
365        }
366    }
367
368    #[test]
369    fn test_canonicalize_body_relaxed_respects_length_limit() {
370        let body = b"\r\n C \t\r\n \t\r\nD \t E\r\n\r\n";
371        let canonical = reference_body_relaxed(body);
372
373        for limit in 0..=canonical.len() + 1 {
374            assert_eq!(
375                body_relaxed_with_limit(body, limit),
376                &canonical[..limit.min(canonical.len())],
377                "limit={limit}"
378            );
379        }
380    }
381
382    #[test]
383    fn test_canonicalize_body_simple() {
384        assert_eq!(body_simple(b"\r\n"), b"\r\n");
385        assert_eq!(body_simple(b"hey        \r\n"), b"hey        \r\n");
386        assert_eq!(
387            body_simple(b" C \r\nD \t E\r\n\r\n\r\n"),
388            b" C \r\nD \t E\r\n"
389        );
390    }
391}