Skip to main content

Crate mail_auth

Crate mail_auth 

Source
Expand description

§mail-auth

crates.io build docs.rs crates.io

mail-auth is an e-mail authentication and reporting 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):
    • ED25519-SHA256 and RSA-SHA256 signing and chain verification.
    • Algorithmic dexterity: a single DKIM2-Signature can carry several signatures under different selectors and algorithms.
    • SMTP envelope (MAIL FROM / RCPT TO) and next-domain chain binding, with per-hop Message-Instance hashes and modification recipes.
    • 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 using the Abuse Reporting Format.
    • Key-pair generation for both RSA and Ed25519 (enabled by the generate feature).
  • Sender Policy Framework (SPF):
    • Policy evaluation.
    • SPF failure reporting using the Abuse Reporting Format.
  • Domain-based Message Authentication, Reporting, and Conformance (DMARC):
    • Policy evaluation.
    • DMARC aggregate report parsing and generation.
  • Authenticated Received Chain (ARC) (now historic):
    • ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification.
    • ARC sealing.
  • Abuse Reporting Format (ARF):
    • Abuse and Authentication failure reporting.
    • Feedback report parsing and generation.
  • SMTP TLS Reporting:
    • Report parsing and generation.

§Backends

§Cryptography

Signing and verification are backed by a selectable crypto provider, chosen with Cargo features:

§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 with MessageAuthenticator::new_cloudflare_tls(), new_system_conf(), and the other new_* 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-crypto

On 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_pkcs1_pem(RSA_PRIVATE_KEY).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(Envelope {
                mail_from: "sender@example.com",
                rcpt_to: &["recipient@example.org"],
            }),
        )
        .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 {
        mail_from: "sender@example.com",
        rcpt_to: &["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_pkcs1_pem(RSA_PRIVATE_KEY).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_bytes(
        &base64_decode(ED25519_PUBLIC_KEY.as_bytes()).unwrap(),
        &base64_decode(ED25519_PRIVATE_KEY.as_bytes()).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::verify_ehlo(
            "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::verify_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::verify_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);

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(), &DkimResult::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_result(&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_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
        let arc_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!("{}{}", arc_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 test

To fuzz the library with cargo-fuzz:

 $ cargo +nightly fuzz run mail_auth

§Conformed RFCs

§DKIM2

§DKIM1

§SPF

§DMARC

§ARF

§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 (C) 2020, Stalwart Labs LLC

Re-exports§

pub use flate2;
pub use hickory_resolver;
pub use zip;

Modules§

common
dkim
dkim2
dmarc
mta_sts
report
spf

Structs§

AuthenticatedMessage
AuthenticationResults
DkimOutput
DmarcOutput
IprevOutput
MX
MessageAuthenticator
Parameters
ReceivedSpf
RecordSet
SpfOutput

Enums§

Dkim2Result
DkimResult
DmarcResult
DnsError
DnssecStatus
Error
IpLookupStrategy
IprevResult
SpfResult
Txt
Version

Traits§

ResolverCache

Type Aliases§

Result