Commit
+109 -14 +/-5 browse
1 | diff --git a/CHANGELOG.md b/CHANGELOG.md |
2 | index e403d90..0dcb310 100644 |
3 | --- a/CHANGELOG.md |
4 | +++ b/CHANGELOG.md |
5 | @@ -1,3 +1,8 @@ |
6 | + mail-auth 0.3.7 |
7 | + ================================ |
8 | + - Fix: Incorrect body hash when content is empty (#22) |
9 | + - Bump to `rustls-pemfile` dependency to 2. |
10 | + |
11 | mail-auth 0.3.6 |
12 | ================================ |
13 | - Bump `hickory-resolver` dependency to 0.24. |
14 | diff --git a/Cargo.toml b/Cargo.toml |
15 | index 1b22df3..8706b91 100644 |
16 | --- a/Cargo.toml |
17 | +++ b/Cargo.toml |
18 | @@ -1,7 +1,7 @@ |
19 | [package] |
20 | name = "mail-auth" |
21 | description = "DKIM, ARC, SPF and DMARC library for Rust" |
22 | - version = "0.3.6" |
23 | + version = "0.3.7" |
24 | edition = "2021" |
25 | authors = [ "Stalwart Labs <hello@stalw.art>"] |
26 | license = "Apache-2.0 OR MIT" |
27 | @@ -25,13 +25,13 @@ ahash = "0.8.0" |
28 | ed25519-dalek = { version = "2.0", optional = true } |
29 | flate2 = "1.0.25" |
30 | lru-cache = "0.1.2" |
31 | - mail-parser = { version = "0.9", git = "https://github.com/stalwartlabs/mail-parser", features = ["ludicrous_mode", "full_encoding"] } |
32 | - mail-builder = { version = "0.3", git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } |
33 | + mail-parser = { version = "0.9", features = ["ludicrous_mode", "full_encoding"] } |
34 | + mail-builder = { version = "0.3", features = ["ludicrous_mode"] } |
35 | parking_lot = "0.12.0" |
36 | - quick-xml = "0.30" |
37 | + quick-xml = "0.31" |
38 | ring = { version = "0.17", optional = true } |
39 | rsa = { version = "0.7", optional = true } |
40 | - rustls-pemfile = { version = "1", optional = true } |
41 | + rustls-pemfile = { version = "2", optional = true } |
42 | serde = { version = "1.0", features = ["derive"] } |
43 | serde_json = "1.0" |
44 | sha1 = { version = "0.10", features = ["oid"], optional = true } |
45 | @@ -41,4 +41,4 @@ zip = "0.6.3" |
46 | |
47 | [dev-dependencies] |
48 | tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] } |
49 | - rustls-pemfile = "1" |
50 | + rustls-pemfile = "2" |
51 | diff --git a/src/common/crypto/ring_impls.rs b/src/common/crypto/ring_impls.rs |
52 | index b4639f4..02c40e9 100644 |
53 | --- a/src/common/crypto/ring_impls.rs |
54 | +++ b/src/common/crypto/ring_impls.rs |
55 | @@ -30,11 +30,11 @@ impl<T: HashImpl> RsaKey<T> { |
56 | .map_err(|err| Error::CryptoError(err.to_string()))?; |
57 | |
58 | let pkcs8_der = match item { |
59 | - Some(rustls_pemfile::Item::PKCS8Key(key)) => key, |
60 | + Some(rustls_pemfile::Item::Pkcs8Key(key)) => key, |
61 | _ => return Err(Error::CryptoError("No PKCS8 key found in PEM".to_string())), |
62 | }; |
63 | |
64 | - Self::from_pkcs8_der(&pkcs8_der) |
65 | + Self::from_pkcs8_der(pkcs8_der.secret_pkcs8_der()) |
66 | } |
67 | |
68 | /// Creates a new RSA private key from PKCS8 DER-encoded bytes. |
69 | @@ -53,11 +53,11 @@ impl<T: HashImpl> RsaKey<T> { |
70 | .map_err(|err| Error::CryptoError(err.to_string()))?; |
71 | |
72 | let rsa_der = match item { |
73 | - Some(rustls_pemfile::Item::RSAKey(key)) => key, |
74 | + Some(rustls_pemfile::Item::Pkcs1Key(key)) => key, |
75 | _ => return Err(Error::CryptoError("No RSA key found in PEM".to_string())), |
76 | }; |
77 | |
78 | - Self::from_der(&rsa_der) |
79 | + Self::from_der(rsa_der.secret_pkcs1_der()) |
80 | } |
81 | |
82 | /// Creates a new RSA private key from a PKCS1 binary slice. |
83 | diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs |
84 | index 92c2c37..e35ec3e 100644 |
85 | --- a/src/dkim/canonicalize.rs |
86 | +++ b/src/dkim/canonicalize.rs |
87 | @@ -24,6 +24,7 @@ impl Writable for CanonicalBody<'_> { |
88 | match self.canonicalization { |
89 | Canonicalization::Relaxed => { |
90 | let mut last_ch = 0; |
91 | + let mut is_empty = true; |
92 | |
93 | for &ch in self.body { |
94 | match ch { |
95 | @@ -32,6 +33,7 @@ impl Writable for CanonicalBody<'_> { |
96 | hasher.write(b"\r\n"); |
97 | crlf_seq -= 1; |
98 | } |
99 | + is_empty = false; |
100 | } |
101 | b'\n' => { |
102 | crlf_seq += 1; |
103 | @@ -48,11 +50,16 @@ impl Writable for CanonicalBody<'_> { |
104 | } |
105 | |
106 | hasher.write(&[ch]); |
107 | + is_empty = false; |
108 | } |
109 | } |
110 | |
111 | last_ch = ch; |
112 | } |
113 | + |
114 | + if !is_empty { |
115 | + hasher.write(b"\r\n"); |
116 | + } |
117 | } |
118 | Canonicalization::Simple => { |
119 | for &ch in self.body { |
120 | @@ -70,10 +77,10 @@ impl Writable for CanonicalBody<'_> { |
121 | } |
122 | } |
123 | } |
124 | + |
125 | + hasher.write(b"\r\n"); |
126 | } |
127 | } |
128 | - |
129 | - hasher.write(b"\r\n"); |
130 | } |
131 | } |
132 | |
133 | @@ -202,9 +209,14 @@ impl<'a> Writable for CanonicalHeaders<'a> { |
134 | |
135 | #[cfg(test)] |
136 | mod test { |
137 | + use mail_builder::encoders::base64::base64_encode; |
138 | + |
139 | use super::{CanonicalBody, CanonicalHeaders}; |
140 | use crate::{ |
141 | - common::headers::{HeaderIterator, Writable}, |
142 | + common::{ |
143 | + crypto::{HashImpl, Sha256}, |
144 | + headers::{HeaderIterator, Writable}, |
145 | + }, |
146 | dkim::Canonicalization, |
147 | }; |
148 | |
149 | @@ -249,7 +261,7 @@ mod test { |
150 | ), |
151 | ( |
152 | concat!("H: value\t\r\n\r\n",), |
153 | - (concat!("h:value\r\n"), concat!("\r\n")), |
154 | + (concat!("h:value\r\n"), concat!("")), |
155 | (concat!("H: value\t\r\n"), concat!("\r\n")), |
156 | ), |
157 | ( |
158 | @@ -257,6 +269,11 @@ mod test { |
159 | (concat!("x:z\r\n"), concat!("abc\r\n")), |
160 | ("\tx\t: \t\t\tz\r\n", concat!("abc\r\n")), |
161 | ), |
162 | + ( |
163 | + concat!("Subject: hello\r\n\r\n\r\n",), |
164 | + (concat!("subject:hello\r\n"), ""), |
165 | + ("Subject: hello\r\n", concat!("\r\n")), |
166 | + ), |
167 | ] { |
168 | let mut header_iterator = HeaderIterator::new(message.as_bytes()); |
169 | let parsed_headers = (&mut header_iterator).collect::<Vec<_>>(); |
170 | @@ -286,5 +303,31 @@ mod test { |
171 | assert_eq!(expected_body, String::from_utf8(body).unwrap()); |
172 | } |
173 | } |
174 | + |
175 | + // Test empty body hashes |
176 | + for (canonicalization, hash) in [ |
177 | + ( |
178 | + Canonicalization::Relaxed, |
179 | + "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", |
180 | + ), |
181 | + ( |
182 | + Canonicalization::Simple, |
183 | + "frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=", |
184 | + ), |
185 | + ] { |
186 | + for body in ["\r\n", ""] { |
187 | + let mut hasher = Sha256::hasher(); |
188 | + CanonicalBody { |
189 | + canonicalization, |
190 | + body: body.as_bytes(), |
191 | + } |
192 | + .write(&mut hasher); |
193 | + |
194 | + assert_eq!( |
195 | + String::from_utf8(base64_encode(hasher.finish().as_ref()).unwrap()).unwrap(), |
196 | + hash, |
197 | + ); |
198 | + } |
199 | + } |
200 | } |
201 | } |
202 | diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs |
203 | index c6dc9d3..99fdf8d 100644 |
204 | --- a/src/dkim/sign.rs |
205 | +++ b/src/dkim/sign.rs |
206 | @@ -199,6 +199,13 @@ mod test { |
207 | "I'm going to need those TPS reports ASAP. ", |
208 | "So, if you could do that, that'd be great.\r\n" |
209 | ); |
210 | + let empty_message = concat!( |
211 | + "From: bill@example.com\r\n", |
212 | + "To: jdoe@example.com\r\n", |
213 | + "Subject: Empty TPS Report\r\n", |
214 | + "\r\n", |
215 | + "\r\n" |
216 | + ); |
217 | let message_multiheader = concat!( |
218 | "X-Duplicate-Header: 4\r\n", |
219 | "From: bill@example.com\r\n", |
220 | @@ -278,6 +285,46 @@ mod test { |
221 | ) |
222 | .await; |
223 | |
224 | + dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message"); |
225 | + #[cfg(feature = "rust-crypto")] |
226 | + let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap(); |
227 | + #[cfg(all(feature = "ring", not(feature = "rust-crypto")))] |
228 | + let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap(); |
229 | + verify( |
230 | + &resolver, |
231 | + DkimSigner::from_key(pk_rsa) |
232 | + .domain("example.com") |
233 | + .selector("default") |
234 | + .headers(["From", "To", "Subject"]) |
235 | + .agent_user_identifier("\"John Doe\" <jdoe@example.com>") |
236 | + .sign(empty_message.as_bytes()) |
237 | + .unwrap(), |
238 | + empty_message, |
239 | + Ok(()), |
240 | + ) |
241 | + .await; |
242 | + |
243 | + dbg!("Test RSA-SHA256 simple/simple with an empty message"); |
244 | + #[cfg(feature = "rust-crypto")] |
245 | + let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap(); |
246 | + #[cfg(all(feature = "ring", not(feature = "rust-crypto")))] |
247 | + let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap(); |
248 | + verify( |
249 | + &resolver, |
250 | + DkimSigner::from_key(pk_rsa) |
251 | + .domain("example.com") |
252 | + .selector("default") |
253 | + .headers(["From", "To", "Subject"]) |
254 | + .header_canonicalization(Canonicalization::Simple) |
255 | + .body_canonicalization(Canonicalization::Simple) |
256 | + .agent_user_identifier("\"John Doe\" <jdoe@example.com>") |
257 | + .sign(empty_message.as_bytes()) |
258 | + .unwrap(), |
259 | + empty_message, |
260 | + Ok(()), |
261 | + ) |
262 | + .await; |
263 | + |
264 | dbg!("Test RSA-SHA256 simple/simple with duplicated headers"); |
265 | #[cfg(feature = "rust-crypto")] |
266 | let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap(); |