Skip to content

Commit 0f7cc11

Browse files
committed
v0.4.0
1 parent 0a0ac88 commit 0f7cc11

File tree

7 files changed

+136
-77
lines changed

7 files changed

+136
-77
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
mail-auth 0.4.0
2+
================================
3+
- DKIM verification defaults to `strict` mode and ignores signatures with a `l=` tag to avoid exploits (see https://stalw.art/blog/dkim-exploit). Use `AuthenticatedMessage::parse_with_opts(&message, false)` to enable `relaxed` mode.
4+
- Parsed fields are now public.
5+
16
mail-auth 0.3.11
27
================================
38
- Added: DKIM keypair generation for both RSA and Ed25519.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "mail-auth"
33
description = "DKIM, ARC, SPF and DMARC library for Rust"
4-
version = "0.3.11"
4+
version = "0.4.0"
55
edition = "2021"
66
authors = [ "Stalwart Labs <[email protected]>"]
77
license = "Apache-2.0 OR MIT"
@@ -38,7 +38,7 @@ serde_json = "1.0"
3838
sha1 = { version = "0.10", features = ["oid"], optional = true }
3939
sha2 = { version = "0.10.6", features = ["oid"], optional = true }
4040
hickory-resolver = { version = "0.24", features = ["dns-over-rustls", "dnssec-ring"] }
41-
zip = "0.6.3"
41+
zip = "1.3.0"
4242
rand = { version = "0.8.5", optional = true }
4343

4444
[dev-dependencies]

src/common/auth_results.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ impl AsAuthResult for Error {
344344
Error::ArcBrokenChain => "broken ARC chain",
345345
Error::NotAligned => "policy not aligned",
346346
Error::InvalidRecordType => "invalid dns record type",
347+
Error::SignatureLength => "signature length ignored due to security risk",
347348
});
348349
header.push(')');
349350
}

src/common/message.rs

Lines changed: 88 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ use super::headers::{AuthenticatedHeader, Header, HeaderParser};
1616

1717
impl<'x> AuthenticatedMessage<'x> {
1818
pub fn parse(raw_message: &'x [u8]) -> Option<Self> {
19+
Self::parse_with_opts(raw_message, true)
20+
}
21+
22+
pub fn parse_with_opts(raw_message: &'x [u8], strict: bool) -> Option<Self> {
1923
let mut message = AuthenticatedMessage {
2024
headers: Vec::new(),
2125
from: Vec::new(),
@@ -35,90 +39,103 @@ impl<'x> AuthenticatedMessage<'x> {
3539
let mut has_arc_errors = false;
3640

3741
for (header, value) in &mut headers {
38-
let name = match header {
39-
AuthenticatedHeader::Ds(name) => {
40-
let signature = dkim::Signature::parse(value);
41-
if let Ok(signature) = &signature {
42-
let ha = HashAlgorithm::from(signature.a);
43-
if !message
44-
.body_hashes
45-
.iter()
46-
.any(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
47-
{
48-
message
49-
.body_hashes
50-
.push((signature.cb, ha, signature.l, Vec::new()));
51-
}
52-
}
53-
message
54-
.dkim_headers
55-
.push(Header::new(name, value, signature));
56-
name
57-
}
58-
AuthenticatedHeader::Aar(name) => {
59-
let results = arc::Results::parse(value);
60-
if !has_arc_errors {
61-
has_arc_errors = results.is_err();
42+
let name =
43+
match header {
44+
AuthenticatedHeader::Ds(name) => {
45+
let signature = match dkim::Signature::parse(value) {
46+
Ok(signature) if signature.l == 0 || !strict => {
47+
let ha = HashAlgorithm::from(signature.a);
48+
if !message.body_hashes.iter().any(|(c, h, l, _)| {
49+
c == &signature.cb && h == &ha && l == &signature.l
50+
}) {
51+
message.body_hashes.push((
52+
signature.cb,
53+
ha,
54+
signature.l,
55+
Vec::new(),
56+
));
57+
}
58+
Ok(signature)
59+
}
60+
Ok(_) => Err(crate::Error::SignatureLength),
61+
Err(err) => Err(err),
62+
};
63+
64+
message
65+
.dkim_headers
66+
.push(Header::new(name, value, signature));
67+
name
6268
}
63-
message.aar_headers.push(Header::new(name, value, results));
64-
name
65-
}
66-
AuthenticatedHeader::Ams(name) => {
67-
let signature = arc::Signature::parse(value);
68-
69-
if let Ok(signature) = &signature {
70-
let ha = HashAlgorithm::from(signature.a);
71-
if !message
72-
.body_hashes
73-
.iter()
74-
.any(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
75-
{
76-
message
77-
.body_hashes
78-
.push((signature.cb, ha, signature.l, Vec::new()));
69+
AuthenticatedHeader::Aar(name) => {
70+
let results = arc::Results::parse(value);
71+
if !has_arc_errors {
72+
has_arc_errors = results.is_err();
7973
}
80-
} else {
81-
has_arc_errors = true;
74+
message.aar_headers.push(Header::new(name, value, results));
75+
name
8276
}
83-
84-
message
85-
.ams_headers
86-
.push(Header::new(name, value, signature));
87-
name
88-
}
89-
AuthenticatedHeader::As(name) => {
90-
let seal = arc::Seal::parse(value);
91-
if !has_arc_errors {
92-
has_arc_errors = seal.is_err();
77+
AuthenticatedHeader::Ams(name) => {
78+
let signature = match arc::Signature::parse(value) {
79+
Ok(signature) if signature.l == 0 || !strict => {
80+
let ha = HashAlgorithm::from(signature.a);
81+
if !message.body_hashes.iter().any(|(c, h, l, _)| {
82+
c == &signature.cb && h == &ha && l == &signature.l
83+
}) {
84+
message.body_hashes.push((
85+
signature.cb,
86+
ha,
87+
signature.l,
88+
Vec::new(),
89+
));
90+
}
91+
Ok(signature)
92+
}
93+
Ok(_) => {
94+
has_arc_errors = true;
95+
Err(crate::Error::SignatureLength)
96+
}
97+
Err(err) => {
98+
has_arc_errors = true;
99+
Err(err)
100+
}
101+
};
102+
103+
message
104+
.ams_headers
105+
.push(Header::new(name, value, signature));
106+
name
93107
}
94-
message.as_headers.push(Header::new(name, value, seal));
95-
name
96-
}
97-
AuthenticatedHeader::From(name) => {
98-
match MessageStream::new(value).parse_address() {
99-
HeaderValue::Address(Address::List(list)) => {
100-
message.from.extend(
101-
list.into_iter()
102-
.filter_map(|a| a.address.map(|a| a.to_lowercase())),
103-
);
108+
AuthenticatedHeader::As(name) => {
109+
let seal = arc::Seal::parse(value);
110+
if !has_arc_errors {
111+
has_arc_errors = seal.is_err();
104112
}
105-
HeaderValue::Address(Address::Group(group_list)) => {
106-
message
113+
message.as_headers.push(Header::new(name, value, seal));
114+
name
115+
}
116+
AuthenticatedHeader::From(name) => {
117+
match MessageStream::new(value).parse_address() {
118+
HeaderValue::Address(Address::List(list)) => {
119+
message.from.extend(
120+
list.into_iter()
121+
.filter_map(|a| a.address.map(|a| a.to_lowercase())),
122+
);
123+
}
124+
HeaderValue::Address(Address::Group(group_list)) => message
107125
.from
108126
.extend(group_list.into_iter().flat_map(|group| {
109127
group
110128
.addresses
111129
.into_iter()
112130
.filter_map(|a| a.address.map(|a| a.to_lowercase()))
113-
}))
131+
})),
132+
_ => (),
114133
}
115-
_ => (),
116-
}
117134

118-
name
119-
}
120-
AuthenticatedHeader::Other(name) => name,
121-
};
135+
name
136+
}
137+
AuthenticatedHeader::Other(name) => name,
138+
};
122139

123140
message.headers.push((name, value));
124141
}

src/dkim/sign.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ impl<'a> Writable for SignableMessage<'a> {
106106
#[cfg(test)]
107107
#[allow(unused)]
108108
pub mod test {
109+
use core::str;
109110
use std::time::{Duration, Instant};
110111

111112
use hickory_resolver::proto::op::ResponseCode;
@@ -351,12 +352,12 @@ pub mod test {
351352
)
352353
.await;
353354

354-
dbg!("Test RSA-SHA256 simple/relaxed with fixed body length");
355+
dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (relaxed)");
355356
#[cfg(feature = "rust-crypto")]
356357
let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
357358
#[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
358359
let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap();
359-
verify(
360+
verify_with_opts(
360361
&resolver,
361362
DkimSigner::from_key(pk_rsa)
362363
.domain("example.com")
@@ -368,6 +369,28 @@ pub mod test {
368369
.unwrap(),
369370
&(message.to_string() + "\r\n----- Mailing list"),
370371
Ok(()),
372+
false,
373+
)
374+
.await;
375+
376+
dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (strict)");
377+
#[cfg(feature = "rust-crypto")]
378+
let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
379+
#[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
380+
let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(RSA_PRIVATE_KEY).unwrap();
381+
verify_with_opts(
382+
&resolver,
383+
DkimSigner::from_key(pk_rsa)
384+
.domain("example.com")
385+
.selector("default")
386+
.headers(["From", "To", "Subject"])
387+
.header_canonicalization(Canonicalization::Simple)
388+
.body_length(true)
389+
.sign(message.as_bytes())
390+
.unwrap(),
391+
&(message.to_string() + "\r\n----- Mailing list"),
392+
Err(super::Error::SignatureLength),
393+
true,
371394
)
372395
.await;
373396

@@ -486,17 +509,18 @@ pub mod test {
486509
.await;
487510
}
488511

489-
pub async fn verify<'x>(
512+
pub async fn verify_with_opts<'x>(
490513
resolver: &Resolver,
491514
signature: Signature,
492515
message_: &'x str,
493516
expect: Result<(), super::Error>,
517+
strict: bool,
494518
) -> Vec<DkimOutput<'x>> {
495519
let mut message = Vec::with_capacity(message_.len() + 100);
496520
signature.write(&mut message, true);
497521
message.extend_from_slice(message_.as_bytes());
498522

499-
let message = AuthenticatedMessage::parse(&message).unwrap();
523+
let message = AuthenticatedMessage::parse_with_opts(&message, strict).unwrap();
500524
let dkim = resolver.verify_dkim(&message).await;
501525

502526
match (dkim.last().unwrap().result(), &expect) {
@@ -517,4 +541,13 @@ pub mod test {
517541
})
518542
.collect()
519543
}
544+
545+
pub async fn verify<'x>(
546+
resolver: &Resolver,
547+
signature: Signature,
548+
message_: &'x str,
549+
expect: Result<(), super::Error>,
550+
) -> Vec<DkimOutput<'x>> {
551+
verify_with_opts(resolver, signature, message_, expect, true).await
552+
}
520553
}

src/dkim/verify.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ impl Resolver {
221221
| Error::ArcInvalidCV
222222
| Error::ArcHasHeaderTag
223223
| Error::ArcBrokenChain
224+
| Error::SignatureLength
224225
| Error::NotAligned => (record.rr & RR_OTHER) != 0,
225226
};
226227

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ pub enum Error {
461461
RevokedPublicKey,
462462
IncompatibleAlgorithms,
463463
SignatureExpired,
464+
SignatureLength,
464465
DnsError(String),
465466
DnsRecordNotFound(ResponseCode),
466467
ArcChainTooLong,
@@ -501,6 +502,7 @@ impl Display for Error {
501502
),
502503
Error::FailedVerification => write!(f, "Signature verification failed"),
503504
Error::SignatureExpired => write!(f, "Signature expired"),
505+
Error::SignatureLength => write!(f, "Insecure 'l=' tag found in Signature"),
504506
Error::FailedAuidMatch => write!(f, "AUID does not match domain name"),
505507
Error::ArcInvalidInstance(i) => {
506508
write!(f, "Invalid 'i={i}' value found in ARC header")

0 commit comments

Comments
 (0)