mail_auth/dkim2/
canonicalize.rs1use crate::{
8 common::{
9 crypto::{HashContext, HashImpl, HashOutput, Sha1, Sha256},
10 headers::Writer,
11 },
12 dkim::Canonicalization,
13};
14use std::cmp::Ordering;
15
16impl crate::common::crypto::HashAlgorithm {
17 pub fn headers_hash<'x>(
19 &self,
20 headers: impl IntoIterator<Item = (&'x [u8], &'x [u8])>,
21 ) -> HashOutput {
22 let mut signed: Vec<(&[u8], &[u8])> = headers
23 .into_iter()
24 .filter(|(name, _)| !is_non_signed_header(name))
25 .collect();
26 signed.reverse();
27 signed.sort_by(|(a, _), (b, _)| cmp_ignore_ascii_case(a, b));
28
29 match self {
30 Self::Sha256 => {
31 let mut hasher = Sha256::hasher();
32 Canonicalization::Relaxed.canonicalize_headers(signed.into_iter(), &mut hasher);
33 hasher.complete()
34 }
35 Self::Sha1 => {
36 let mut hasher = Sha1::hasher();
37 Canonicalization::Relaxed.canonicalize_headers(signed.into_iter(), &mut hasher);
38 hasher.complete()
39 }
40 }
41 }
42
43 pub fn body_hash(&self, body: &[u8]) -> HashOutput {
45 self.hash(Canonicalization::Simple.canonical_body(body, u64::MAX))
46 }
47}
48
49pub(crate) struct CanonicalizedHeaderWriter<'x, W: Writer> {
50 inner: &'x mut W,
51}
52
53impl<'x, W: Writer> CanonicalizedHeaderWriter<'x, W> {
54 pub fn new(inner: &'x mut W, field: &[u8]) -> Self {
55 for &ch in field {
56 if !ch.is_ascii_whitespace() {
57 inner.write(&[ch.to_ascii_lowercase()]);
58 }
59 }
60 inner.write(b":");
61
62 Self { inner }
63 }
64
65 pub fn finalize(self) {
66 self.inner.write(b"\r\n");
67 }
68}
69
70impl<'x, W: Writer> Writer for CanonicalizedHeaderWriter<'x, W> {
71 fn write(&mut self, buf: &[u8]) {
72 for &ch in buf {
73 if !ch.is_ascii_whitespace() {
74 self.inner.write(&[ch]);
75 }
76 }
77 }
78}
79
80pub(crate) fn cmp_ignore_ascii_case(a: &[u8], b: &[u8]) -> Ordering {
81 a.iter()
82 .map(u8::to_ascii_lowercase)
83 .cmp(b.iter().map(u8::to_ascii_lowercase))
84}
85
86pub(super) fn is_non_signed_header(name: &[u8]) -> bool {
87 let name = name.trim_ascii();
88 hashify::tiny_map_ignore_case!(name,
89 b"received" => true,
90 b"return-path" => true,
91 b"delivered-to" => true,
92 b"authentication-results" => true,
93 b"dkim-signature" => true,
94 b"message-instance" => true,
95 b"dkim2-signature" => true,
96 b"arc-authentication-results" => true,
97 b"arc-message-signature" => true,
98 b"arc-seal" => true
99 )
100 .unwrap_or_else(|| {
101 matches!(name.get(1), Some(&b'-')) && matches!(name.first(), Some(&b'x' | &b'X'))
102 })
103}
104
105#[cfg(test)]
106mod test {
107 use super::is_non_signed_header;
108 use crate::common::crypto::HashAlgorithm;
109
110 #[test]
111 fn excluded_headers_are_classified() {
112 for name in [
113 "received",
114 "Received",
115 "return-path",
116 "delivered-to",
117 "Delivered-To",
118 "authentication-results",
119 "dkim-signature",
120 "DKIM-Signature",
121 "message-instance",
122 "dkim2-signature",
123 "arc-seal",
124 "arc-message-signature",
125 "arc-authentication-results",
126 "x-spam-score",
127 "X-Anything",
128 ] {
129 assert!(
130 is_non_signed_header(name.as_bytes()),
131 "{name} must be ignored"
132 );
133 }
134 for name in [
135 "from",
136 "to",
137 "subject",
138 "date",
139 "message-id",
140 "list-unsubscribe",
141 ] {
142 assert!(
143 !is_non_signed_header(name.as_bytes()),
144 "{name} must be signed"
145 );
146 }
147 }
148
149 #[test]
150 fn body_hash_ignores_trailing_blank_lines() {
151 let none = HashAlgorithm::Sha256.body_hash(b"Hello, world.");
152 let one = HashAlgorithm::Sha256.body_hash(b"Hello, world.\r\n");
153 let many = HashAlgorithm::Sha256.body_hash(b"Hello, world.\r\n\r\n\r\n");
154 assert_eq!(none, one);
155 assert_eq!(one, many);
156 }
157
158 #[test]
159 fn empty_body_hashes_as_single_crlf() {
160 assert_eq!(
161 HashAlgorithm::Sha256.body_hash(b""),
162 HashAlgorithm::Sha256.body_hash(b"\r\n\r\n")
163 );
164 }
165}