Commit
Author: mdecimus [mauro@stalw.art]
Hash: ea424c40112dbaff03be7b395d81a681ae34fe08
Timestamp: Thu, 28 Dec 2023 10:08:05 +0000 (10 months ago)

+109 -14 +/-5 browse
v0.3.7
1diff --git a/CHANGELOG.md b/CHANGELOG.md
2index 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
15index 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
52index 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
84index 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
203index 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();