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