Expand description
§mail-auth
mail-auth is an e-mail authentication library written in Rust that supports the DKIM2, DKIM1, ARC, SPF and DMARC protocols. The library aims to be fast, safe and correct while supporting all major message authentication and reporting RFCs.
Features:
- DomainKeys Identified Mail v2 (DKIMv2):
- Implements draft-ietf-dkim-dkim2-spec-08.
- ED25519-SHA256, RSA-SHA256 and post-quantum ML-DSA-44 signing and chain verification, with SHA-256 and SHA-512 message hashes.
- Algorithmic agility: a single
DKIM2-Signaturecan carry several signatures under different selectors and algorithms, combined as alternatives (|) or as hybrids that must all verify (,). - SMTP envelope (MAIL FROM / RCPT TO) and next-domain chain binding, with per-hop
Message-Instancehashes and modification recipes. - Authentic
Fromchecks, full and provisional (§13.2) verification, and a signer that only extends Fully Verified DKIM2 Messages. - Verifiable delivery status notification (DSN) authentication.
- DomainKeys Identified Mail v1 (DKIMv1):
- ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 signing and verification.
- DKIM Authorized Third-Party Signatures.
- DKIM failure reporting records (RFC 6651).
- Key-pair generation for RSA, Ed25519 and the DKIM2 ML-DSA-44 algorithm (enabled by the
generatefeature).
- Sender Policy Framework (SPF):
- Policy evaluation.
- SPF failure reporting modifiers (RFC 6652).
- Domain-based Message Authentication, Reporting, and Conformance (DMARC):
- Policy evaluation.
- Authenticated Received Chain (ARC) (now historic):
- ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification.
- ARC sealing.
- SMTP TLS Reporting:
- TLS-RPT policy record parsing.
§Backends
§Cryptography
Signing and verification are backed by a selectable crypto provider, chosen with Cargo features:
aws-lc-rs(default): uses aws-lc-rs, and aws-lc-sys directly for ML-DSA-44 with a signature context. Theaws-lc-sysrequirement always matches the one of the requiredaws-lc-rsrelease; if a neweraws-lc-rsmoves to anotheraws-lc-sys, both copies of AWS-LC are built and linked until mail-auth follows.rust-crypto: a pure-Rust backend (rsa, ed25519-dalek, ml-dsa, sha1, sha2) that compiles to WebAssembly. Used whenaws-lc-rsis disabled.
§DNS resolution
DNS lookups (used by SPF, DMARC, and DKIM/DKIM2 key retrieval) are backed by a selectable resolver, chosen with two mutually exclusive Cargo features:
dns-hickory(default): system-configured UDP/TCP/TLS DNS via hickory-resolver. Build resolvers withMessageAuthenticator::new_cloudflare_tls(),new_system_conf(), and the othernew_*constructors. Not available on WebAssembly.dns-doh: DNS-over-HTTPS (JSON API) via reqwest, usable on both native targets and WebAssembly.
Enable exactly one; building with both, or with neither, is a compile error.
§WebAssembly (WASM)
mail-auth compiles and runs on wasm32-unknown-unknown (browsers, edge runtimes). The WASM-capable configuration pairs the pure-Rust crypto backend with DNS-over-HTTPS:
$ cargo build --target wasm32-unknown-unknown \
--no-default-features --features dns-doh,rust-cryptoOn WASM, DoH requests are issued through the browser Fetch API, randomness comes from the Web Crypto API, and time (cache expiry and signature timestamps) uses the browser clock via web-time.
§Usage examples
§DKIM2 Signing
// Sign a message with both RSA-SHA256 and Ed25519-SHA256 under different
// selectors (algorithmic dexterity).
let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap()).unwrap();
let pk_ed = Ed25519Key::from_pkcs8_der(&ED25519_PKCS8).unwrap();
let signed = Dkim2Signer::from_key(pk_rsa)
.domain("example.com")
.selector("rsa-sel")
.additional_key(pk_ed, "ed-sel")
.sign(
RFC5322_MESSAGE.as_bytes(),
Hop::real("sender@example.com", ["recipient@example.org"]),
// A message that already carries DKIM2 header fields is only signed
// with the passing output of `verify_dkim2` for it.
None,
)
.unwrap();
// Prepend the DKIM2-Signature (and a Message-Instance, if needed) to the message
println!("{}{}", signed.to_header(), RFC5322_MESSAGE);§DKIM2 Chain Verification
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Parse message
let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
// DKIM2 binds the SMTP envelope, so verification needs the MAIL FROM and RCPT TO
let envelope = Envelope::new("sender@example.com", ["recipient@example.org"]);
// Validate the DKIM2 signature chain
let result = authenticator
.verify_dkim2(&authenticated_message, envelope)
.await;
// Make sure the chain passed verification
assert_eq!(result.result(), &Dkim2Result::Pass);§DKIM1 Signature Verification
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Parse message
let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
// Validate signature
let result = authenticator.verify_dkim(&authenticated_message).await;
// Make sure all signatures passed verification
assert!(result.iter().all(|s| s.result() == &DkimResult::Pass));§DKIM1 Signing
// Sign an e-mail message using RSA-SHA256
let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap()).unwrap();
let signature_rsa = DkimSigner::from_key(pk_rsa)
.domain("example.com")
.selector("default")
.headers(["From", "To", "Subject"])
.sign(RFC5322_MESSAGE.as_bytes())
.unwrap();
// Sign an e-mail message using ED25519-SHA256
let pk_ed = Ed25519Key::from_seed_and_public_key(
&base64::LENIENT.decode(ED25519_PRIVATE_KEY).unwrap(),
&base64::LENIENT.decode(ED25519_PUBLIC_KEY).unwrap(),
)
.unwrap();
let signature_ed = DkimSigner::from_key(pk_ed)
.domain("example.com")
.selector("default-ed")
.headers(["From", "To", "Subject"])
.sign(RFC5322_MESSAGE.as_bytes())
.unwrap();
// Print the message including both signatures to stdout
println!(
"{}{}{}",
signature_rsa.to_header(),
signature_ed.to_header(),
RFC5322_MESSAGE
);§SPF Policy Evaluation
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Verify HELO identity
let result = authenticator
.verify_spf(SpfParameters::helo(
"127.0.0.1".parse().unwrap(),
"gmail.com",
"my-local-domain.org",
))
.await;
assert_eq!(result.result(), SpfResult::Fail);
// Verify MAIL-FROM identity
let result = authenticator
.verify_spf(SpfParameters::mail_from(
"::1".parse().unwrap(),
"gmail.com",
"my-local-domain.org",
"sender@gmail.com",
))
.await;
assert_eq!(result.result(), SpfResult::Fail);§DMARC Policy Evaluation
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Verify DKIM signatures
let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
let dkim_result = authenticator.verify_dkim(&authenticated_message).await;
// Verify SPF MAIL-FROM identity
let spf_result = authenticator
.verify_spf(SpfParameters::mail_from(
"::1".parse().unwrap(),
"example.org",
"my-host-domain.org",
"sender@example.org",
))
.await;
// Verify DMARC
let dmarc_result = authenticator
.verify_dmarc(DmarcParameters::new(
&authenticated_message,
&dkim_result,
"example.org",
&spf_result,
))
.await;
assert_eq!(dmarc_result.dkim_result(), &DmarcResult::Pass);
assert_eq!(dmarc_result.spf_result(), &DmarcResult::Pass);
// Build the Authentication-Results and Received-SPF headers
let spf_params = SpfParameters::mail_from(
"::1".parse().unwrap(),
"example.org",
"my-host-domain.org",
"sender@example.org",
);
let auth_results = AuthenticationResults::new("my-host-domain.org")
.with_dkim_results(&dkim_result, authenticated_message.first_from_address())
.with_spf_result(&spf_result, &spf_params)
.with_dmarc_result(&dmarc_result);
let received_spf = ReceivedSpf::new(&spf_result, &spf_params);§DNS Caching
Every verifier accepts either its input alone or a Parameters value that also
carries a DNS cache. Implement DnsCache once on the type that owns your
per-record-type caches. Negative answers (NXDOMAIN and NODATA, RFC 2308) are
cached as well: as TxtRecord::Error in the TXT cache and as Err(Negative)
in the others, for the negative TTL of the answer capped by
MessageAuthenticator::with_max_negative_ttl (one hour by default):
type Answer<T> = Result<RecordSet<T>, Negative>;
struct Caches {
txt: MyCache<Box<str>, TxtRecord>,
mx: MyCache<Box<str>, Answer<Mx>>,
ipv4: MyCache<Box<str>, Answer<Ipv4Addr>>,
ipv6: MyCache<Box<str>, Answer<Ipv6Addr>>,
ptr: MyCache<IpAddr, Answer<Box<str>>>,
}
impl DnsCache for Caches {
type Txt = MyCache<Box<str>, TxtRecord>;
type Mx = MyCache<Box<str>, Answer<Mx>>;
type Ipv4 = MyCache<Box<str>, Answer<Ipv4Addr>>;
type Ipv6 = MyCache<Box<str>, Answer<Ipv6Addr>>;
type Ptr = MyCache<IpAddr, Answer<Box<str>>>;
fn txt(&self) -> Option<&Self::Txt> { Some(&self.txt) }
fn mx(&self) -> Option<&Self::Mx> { Some(&self.mx) }
fn ipv4(&self) -> Option<&Self::Ipv4> { Some(&self.ipv4) }
fn ipv6(&self) -> Option<&Self::Ipv6> { Some(&self.ipv6) }
fn ptr(&self) -> Option<&Self::Ptr> { Some(&self.ptr) }
}
let result = authenticator
.verify_dkim(Parameters::new(&authenticated_message).with_cache(&caches))
.await;More examples available under the examples directory.
§ARC Chain Verification
ARC has been reclassified as Historic, the experiment is over and implementers should not rely on it going forward, with DKIM2 as its intended successor. It remains available, but is opt-in behind the arc feature and is no longer enabled by default. New deployments should prefer DKIM2.
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Parse message
let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
// Validate ARC chain
let result = authenticator.verify_arc(&authenticated_message).await;
// Make sure ARC passed verification
assert_eq!(result.result(), &ArcResult::Pass);§ARC Chain Sealing
// Create an authenticator using Cloudflare DNS
let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
// Parse message to be sealed
let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
// Verify ARC and DKIM signatures
let arc_result = authenticator.verify_arc(&authenticated_message).await;
let dkim_result = authenticator.verify_dkim(&authenticated_message).await;
// Build Authenticated-Results header
let auth_results = AuthenticationResults::new("mx.mydomain.org")
.with_dkim_results(&dkim_result, "sender@example.org")
.with_arc_result(&arc_result, "127.0.0.1".parse().unwrap());
// Seal message
if arc_result.can_be_sealed() {
// Seal the e-mail message using RSA-SHA256
let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap()).unwrap();
let sealed_set = ArcSealer::from_key(pk_rsa)
.domain("example.org")
.selector("default")
.headers(["From", "To", "Subject", "DKIM-Signature"])
.seal(&authenticated_message, &auth_results, &arc_result)
.unwrap();
// Print the sealed message to stdout
println!("{}{}", sealed_set.to_header(), RFC5322_MESSAGE)
} else {
eprintln!("The message could not be sealed, probably an ARC chain with cv=fail was found.")
}§Testing & Fuzzing
To run the testsuite:
$ cargo testTo fuzz the library with cargo-fuzz:
$ cargo +nightly fuzz run mail_auth§Conformed RFCs
§DKIM2
- draft-ietf-dkim-dkim2-spec-08 - DKIM2 Signatures
- draft-ietf-dkim-dkim2-dns - DKIM2 DNS Records
- FIPS 204 - Module-Lattice-Based Digital Signature Standard (ML-DSA-44)
- RFC 9881 - ML-DSA in X.509 (ML-DSA-44 private key encoding)
§DKIM1
- RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures
- RFC 6541 - DomainKeys Identified Mail (DKIM) Authorized Third-Party Signatures
- RFC 6651 - Extensions to DomainKeys Identified Mail (DKIM) for Failure Reporting
- RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC 4686 - Analysis of Threats Motivating DomainKeys Identified Mail (DKIM)
- RFC 5016 - Requirements for a DomainKeys Identified Mail (DKIM) Signing Practices Protocol
- RFC 5585 - DomainKeys Identified Mail (DKIM) Service Overview
- RFC 5672 - DomainKeys Identified Mail (DKIM) Signatures – Update
- RFC 5863 - DomainKeys Identified Mail (DKIM) Development, Deployment, and Operations
- RFC 6377 - DomainKeys Identified Mail (DKIM) and Mailing Lists
§SPF
- RFC 7208 - Sender Policy Framework (SPF)
- RFC 6652 - Sender Policy Framework (SPF) Authentication Failure Reporting Using the Abuse Reporting Format
§DMARC
- RFC 9989 - Domain-based Message Authentication, Reporting, and Conformance (DMARC)
- RFC 9990 - DMARC Aggregate Reporting
- RFC 9991 - DMARC Failure Reporting
- RFC 8617 - The Authenticated Received Chain (ARC) Protocol (being reclassified as Historic)
- RFC 8601 - Message Header Field for Indicating Message Authentication Status
- RFC 8616 - Email Authentication for Internationalized Mail
- RFC 7960 - Interoperability Issues between Domain-based Message Authentication, Reporting, and Conformance (DMARC) and Indirect Email Flows
§SMTP TLS Reporting
§License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
§Copyright
Copyright (C) 2020, Stalwart Labs LLC
Re-exports§
pub use auth_results::AuthenticationResults;pub use auth_results::ReceivedSpf;pub use dkim::DkimOutput;pub use dkim::DkimResult;pub use dkim2::Dkim2Output;pub use dkim2::Dkim2Result;pub use dmarc::DmarcOutput;pub use dmarc::DmarcResult;pub use dns::DnsCache;pub use dns::DnssecStatus;pub use dns::IpLookupStrategy;pub use dns::Mx;pub use dns::Negative;pub use dns::NoCache;pub use dns::Parameters;pub use dns::RecordSet;pub use dns::ResolverCache;pub use dns::TxtRecord;pub use iprev::IprevOutput;pub use iprev::IprevResult;pub use message::AuthenticatedMessage;pub use spf::SpfOutput;pub use spf::SpfResult;pub use hickory_resolver;
Modules§
- auth_
results - Builders for the
Authentication-ResultsandReceived-SPFheaders. - crypto
- Signing keys, verification keys and hash functions.
- dkim
- DKIM1 signing and verification.
- dkim2
- DomainKeys Identified Mail version 2 (DKIM2).
- dmarc
- Domain-based Message Authentication, Reporting, and Conformance (DMARC).
- dns
- DNS resolution and caching.
- headers
- Header field types and output traits shared by every protocol module.
- iprev
- Reverse IP address verification (
iprev, RFC 8601 Section 3). - message
- Parsed view of an RFC 5322 message for authentication.
- mta_sts
- Parsers for the MTA-STS and SMTP TLS Reporting DNS TXT records.
- signer
- Typestate markers for the signer builders.
- spf
- Sender Policy Framework (SPF).
Structs§
- Message
Authenticator - Entry point for every verifier and DNS lookup in this crate.
Enums§
- DnsError
- A DNS lookup failure, carried by
Error::Dns. - Error
- The error type for every fallible operation in this crate.
Type Aliases§
- Result
- A
std::result::Resultwith the error type fixed toError.