Skip to content

Commit

Permalink
v0.3.7
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Dec 28, 2023
1 parent 8a1b216 commit ea424c4
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
mail-auth 0.3.7
================================
- Fix: Incorrect body hash when content is empty (#22)
- Bump to `rustls-pemfile` dependency to 2.

mail-auth 0.3.6
================================
- Bump `hickory-resolver` dependency to 0.24.
Expand Down
12 changes: 6 additions & 6 deletions Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "mail-auth"
description = "DKIM, ARC, SPF and DMARC library for Rust"
version = "0.3.6"
version = "0.3.7"
edition = "2021"
authors = [ "Stalwart Labs <hello@stalw.art>"]
license = "Apache-2.0 OR MIT"
Expand All @@ -25,13 +25,13 @@ ahash = "0.8.0"
ed25519-dalek = { version = "2.0", optional = true }
flate2 = "1.0.25"
lru-cache = "0.1.2"
mail-parser = { version = "0.9", git = "https://github.com/stalwartlabs/mail-parser", features = ["ludicrous_mode", "full_encoding"] }
mail-builder = { version = "0.3", git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
mail-parser = { version = "0.9", features = ["ludicrous_mode", "full_encoding"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
parking_lot = "0.12.0"
quick-xml = "0.30"
quick-xml = "0.31"
ring = { version = "0.17", optional = true }
rsa = { version = "0.7", optional = true }
rustls-pemfile = { version = "1", optional = true }
rustls-pemfile = { version = "2", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha1 = { version = "0.10", features = ["oid"], optional = true }
Expand All @@ -41,4 +41,4 @@ zip = "0.6.3"

[dev-dependencies]
tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] }
rustls-pemfile = "1"
rustls-pemfile = "2"
8 changes: 4 additions & 4 deletions src/common/crypto/ring_impls.rs
Expand Up @@ -30,11 +30,11 @@ impl<T: HashImpl> RsaKey<T> {
.map_err(|err| Error::CryptoError(err.to_string()))?;

let pkcs8_der = match item {
Some(rustls_pemfile::Item::PKCS8Key(key)) => key,
Some(rustls_pemfile::Item::Pkcs8Key(key)) => key,
_ => return Err(Error::CryptoError("No PKCS8 key found in PEM".to_string())),
};

Self::from_pkcs8_der(&pkcs8_der)
Self::from_pkcs8_der(pkcs8_der.secret_pkcs8_der())
}

/// Creates a new RSA private key from PKCS8 DER-encoded bytes.
Expand All @@ -53,11 +53,11 @@ impl<T: HashImpl> RsaKey<T> {
.map_err(|err| Error::CryptoError(err.to_string()))?;

let rsa_der = match item {
Some(rustls_pemfile::Item::RSAKey(key)) => key,
Some(rustls_pemfile::Item::Pkcs1Key(key)) => key,
_ => return Err(Error::CryptoError("No RSA key found in PEM".to_string())),
};

Self::from_der(&rsa_der)
Self::from_der(rsa_der.secret_pkcs1_der())
}

/// Creates a new RSA private key from a PKCS1 binary slice.
Expand Down
51 changes: 47 additions & 4 deletions src/dkim/canonicalize.rs
Expand Up @@ -24,6 +24,7 @@ impl Writable for CanonicalBody<'_> {
match self.canonicalization {
Canonicalization::Relaxed => {
let mut last_ch = 0;
let mut is_empty = true;

for &ch in self.body {
match ch {
Expand All @@ -32,6 +33,7 @@ impl Writable for CanonicalBody<'_> {
hasher.write(b"\r\n");
crlf_seq -= 1;
}
is_empty = false;
}
b'\n' => {
crlf_seq += 1;
Expand All @@ -48,11 +50,16 @@ impl Writable for CanonicalBody<'_> {
}

hasher.write(&[ch]);
is_empty = false;
}
}

last_ch = ch;
}

if !is_empty {
hasher.write(b"\r\n");
}
}
Canonicalization::Simple => {
for &ch in self.body {
Expand All @@ -70,10 +77,10 @@ impl Writable for CanonicalBody<'_> {
}
}
}

hasher.write(b"\r\n");
}
}

hasher.write(b"\r\n");
}
}

Expand Down Expand Up @@ -202,9 +209,14 @@ impl<'a> Writable for CanonicalHeaders<'a> {

#[cfg(test)]
mod test {
use mail_builder::encoders::base64::base64_encode;

use super::{CanonicalBody, CanonicalHeaders};
use crate::{
common::headers::{HeaderIterator, Writable},
common::{
crypto::{HashImpl, Sha256},
headers::{HeaderIterator, Writable},
},
dkim::Canonicalization,
};

Expand Down Expand Up @@ -249,14 +261,19 @@ mod test {
),
(
concat!("H: value\t\r\n\r\n",),
(concat!("h:value\r\n"), concat!("\r\n")),
(concat!("h:value\r\n"), concat!("")),
(concat!("H: value\t\r\n"), concat!("\r\n")),
),
(
concat!("\tx\t: \t\t\tz\r\n\r\nabc",),
(concat!("x:z\r\n"), concat!("abc\r\n")),
("\tx\t: \t\t\tz\r\n", concat!("abc\r\n")),
),
(
concat!("Subject: hello\r\n\r\n\r\n",),
(concat!("subject:hello\r\n"), ""),
("Subject: hello\r\n", concat!("\r\n")),
),
] {
let mut header_iterator = HeaderIterator::new(message.as_bytes());
let parsed_headers = (&mut header_iterator).collect::<Vec<_>>();
Expand Down Expand Up @@ -286,5 +303,31 @@ mod test {
assert_eq!(expected_body, String::from_utf8(body).unwrap());
}
}

// Test empty body hashes
for (canonicalization, hash) in [
(
Canonicalization::Relaxed,
"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
),
(
Canonicalization::Simple,
"frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=",
),
] {
for body in ["\r\n", ""] {
let mut hasher = Sha256::hasher();
CanonicalBody {
canonicalization,
body: body.as_bytes(),
}
.write(&mut hasher);

assert_eq!(
String::from_utf8(base64_encode(hasher.finish().as_ref()).unwrap()).unwrap(),
hash,
);
}
}
}
}
47 changes: 47 additions & 0 deletions src/dkim/sign.rs
Expand Up @@ -199,6 +199,13 @@ mod test {
"I'm going to need those TPS reports ASAP. ",
"So, if you could do that, that'd be great.\r\n"
);
let empty_message = concat!(
"From: bill@example.com\r\n",
"To: jdoe@example.com\r\n",
"Subject: Empty TPS Report\r\n",
"\r\n",
"\r\n"
);
let message_multiheader = concat!(
"X-Duplicate-Header: 4\r\n",
"From: bill@example.com\r\n",
Expand Down Expand Up @@ -278,6 +285,46 @@ mod test {
)
.await;

dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message");
#[cfg(feature = "rust-crypto")]
let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
#[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap();
verify(
&resolver,
DkimSigner::from_key(pk_rsa)
.domain("example.com")
.selector("default")
.headers(["From", "To", "Subject"])
.agent_user_identifier("\"John Doe\" <jdoe@example.com>")
.sign(empty_message.as_bytes())
.unwrap(),
empty_message,
Ok(()),
)
.await;

dbg!("Test RSA-SHA256 simple/simple with an empty message");
#[cfg(feature = "rust-crypto")]
let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
#[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap();
verify(
&resolver,
DkimSigner::from_key(pk_rsa)
.domain("example.com")
.selector("default")
.headers(["From", "To", "Subject"])
.header_canonicalization(Canonicalization::Simple)
.body_canonicalization(Canonicalization::Simple)
.agent_user_identifier("\"John Doe\" <jdoe@example.com>")
.sign(empty_message.as_bytes())
.unwrap(),
empty_message,
Ok(()),
)
.await;

dbg!("Test RSA-SHA256 simple/simple with duplicated headers");
#[cfg(feature = "rust-crypto")]
let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
Expand Down

0 comments on commit ea424c4

Please sign in to comment.