mail-auth 0.14.1

DKIM, ARC, SPF and DMARC library for Rust
Documentation
# mail-auth

[![crates.io](https://img.shields.io/crates/v/mail-auth)](https://crates.io/crates/mail-auth)
[![build](https://github.com/stalwartlabs/mail-auth/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/mail-auth/actions/workflows/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/mail-auth)](https://docs.rs/mail-auth)
[![crates.io](https://img.shields.io/crates/l/mail-auth)](http://www.apache.org/licenses/LICENSE-2.0)

_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](#conformed-rfcs). 

Features:

- **DomainKeys Identified Mail v2 (DKIMv2)**:
  - Implements [draft-ietf-dkim-dkim2-spec-07]https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-spec/.
  - ED25519-SHA256, RSA-SHA256 and post-quantum MLDSA44-Ed25519-SHA512 (ML-DSA-44 + Ed25519 composite) signing and chain verification, with SHA-256 and SHA-512 message hashes.
  - 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.
  - Authentic `From` checks, full and provisional (ยง13.1) verification, and a signer that only extends Validly Signed 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 MLDSA44-Ed25519 composite (enabled by the `generate` feature).
- **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]https://datatracker.ietf.org/doc/draft-ietf-dmarc-arc-to-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]https://crates.io/crates/aws-lc-rs, and [aws-lc-sys]https://crates.io/crates/aws-lc-sys directly for ML-DSA-44 with a signature context. The `aws-lc-sys` requirement always matches the one of the required `aws-lc-rs` release; if a newer `aws-lc-rs` moves to another `aws-lc-sys`, both copies of AWS-LC are built and linked until mail-auth follows.
- `rust-crypto`: a pure-Rust backend ([rsa]https://crates.io/crates/rsa, [ed25519-dalek]https://crates.io/crates/ed25519-dalek, [ml-dsa]https://crates.io/crates/ml-dsa, [sha1]https://crates.io/crates/sha1, [sha2]https://crates.io/crates/sha2) that compiles to WebAssembly. Used when `aws-lc-rs` is 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]https://crates.io/crates/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]https://crates.io/crates/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:

```bash
 $ 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](https://crates.io/crates/web-time).

## Usage examples

### DKIM2 Signing

```rust
    // 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

```rust
    // 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

```rust
    // 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

```rust
    // 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

```rust
    // 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

```rust
    // 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):

```rust
    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](https://github.com/stalwartlabs/mail-auth/tree/main/examples) directory.

### ARC Chain Verification

ARC has been [reclassified as Historic](https://datatracker.ietf.org/doc/draft-ietf-dmarc-arc-to-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.

```rust
    // 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

```rust
    // 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:

```bash
 $ cargo test
```

To fuzz the library with `cargo-fuzz`:

```bash
 $ cargo +nightly fuzz run mail_auth
```

## Conformed RFCs

### DKIM2
- [draft-ietf-dkim-dkim2-spec-07 - DKIM2 Signatures]https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-spec/
- [draft-ietf-dkim-dkim2-dns - DKIM2 DNS Records]https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-dns/
- [draft-ietf-lamps-pq-composite-sigs - Composite ML-DSA]https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-sigs/ (`id-MLDSA44-Ed25519-SHA512`)

### DKIM1
- [RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures]https://datatracker.ietf.org/doc/html/rfc6376
- [RFC 6541 - DomainKeys Identified Mail (DKIM) Authorized Third-Party Signatures]https://datatracker.ietf.org/doc/html/rfc6541
- [RFC 6651 - Extensions to DomainKeys Identified Mail (DKIM) for Failure Reporting]https://datatracker.ietf.org/doc/html/rfc6651
- [RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA)]https://datatracker.ietf.org/doc/html/rfc8032
- [RFC 4686 - Analysis of Threats Motivating DomainKeys Identified Mail (DKIM)]https://datatracker.ietf.org/doc/html/rfc4686
- [RFC 5016 - Requirements for a DomainKeys Identified Mail (DKIM) Signing Practices Protocol]https://datatracker.ietf.org/doc/html/rfc5016
- [RFC 5585 - DomainKeys Identified Mail (DKIM) Service Overview]https://datatracker.ietf.org/doc/html/rfc5585
- [RFC 5672 - DomainKeys Identified Mail (DKIM) Signatures -- Update]https://datatracker.ietf.org/doc/html/rfc5672
- [RFC 5863 - DomainKeys Identified Mail (DKIM) Development, Deployment, and Operations]https://datatracker.ietf.org/doc/html/rfc5863
- [RFC 6377 - DomainKeys Identified Mail (DKIM) and Mailing Lists]https://datatracker.ietf.org/doc/html/rfc6377

### SPF
- [RFC 7208 - Sender Policy Framework (SPF)]https://datatracker.ietf.org/doc/html/rfc7208
- [RFC 6652 - Sender Policy Framework (SPF) Authentication Failure Reporting Using the Abuse Reporting Format]https://datatracker.ietf.org/doc/html/rfc6652

### DMARC
- [RFC 9989 - Domain-based Message Authentication, Reporting, and Conformance (DMARC)]https://datatracker.ietf.org/doc/html/rfc9989
- [RFC 9990 - DMARC Aggregate Reporting]https://datatracker.ietf.org/doc/html/rfc9990
- [RFC 9991 - DMARC Failure Reporting]https://datatracker.ietf.org/doc/html/rfc9991
- [RFC 8617 - The Authenticated Received Chain (ARC) Protocol]https://datatracker.ietf.org/doc/html/rfc8617 (being reclassified as [Historic]https://datatracker.ietf.org/doc/draft-ietf-dmarc-arc-to-historic/)
- [RFC 8601 - Message Header Field for Indicating Message Authentication Status]https://datatracker.ietf.org/doc/html/rfc8601
- [RFC 8616 - Email Authentication for Internationalized Mail]https://datatracker.ietf.org/doc/html/rfc8616
- [RFC 7960 - Interoperability Issues between Domain-based Message Authentication, Reporting, and Conformance (DMARC) and Indirect Email Flows]https://datatracker.ietf.org/doc/html/rfc7960

### SMTP TLS Reporting
- [RFC 8460 - SMTP TLS Reporting]https://datatracker.ietf.org/doc/html/rfc8460

## License

Licensed under either of

 * Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
 * MIT license ([LICENSE-MIT]LICENSE-MIT or <http://opensource.org/licenses/MIT>)

at your option.

## Copyright

Copyright (C) 2020, Stalwart Labs LLC