Code Monkey home page Code Monkey logo

mail-auth's Introduction

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 DKIM, 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 (DKIM):
    • 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).
  • Authenticated Received Chain (ARC):
    • ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification.
    • ARC sealing.
  • 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.
  • Abuse Reporting Format (ARF):
    • Abuse and Authentication failure reporting.
    • Feedback report parsing and generation.
  • SMTP TLS Reporting:
    • Report parsing and generation.

Usage examples

DKIM Signature Verification

    // Create a resolver using Cloudflare DNS
    let resolver = Resolver::new_cloudflare_tls().unwrap();

    // Parse message
    let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();

    // Validate signature
    let result = resolver.verify_dkim(&authenticated_message).await;

    // Make sure all signatures passed verification
    assert!(result.iter().all(|s| s.result() == &DkimResult::Pass));

DKIM 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
    );

ARC Chain Verification

    // Create a resolver using Cloudflare DNS
    let resolver = Resolver::new_cloudflare_tls().unwrap();

    // Parse message
    let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();

    // Validate ARC chain
    let result = resolver.verify_arc(&authenticated_message).await;

    // Make sure ARC passed verification
    assert_eq!(result.result(), &DkimResult::Pass);

ARC Chain Sealing

    // Create a resolver using Cloudflare DNS
    let resolver = Resolver::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 = resolver.verify_arc(&authenticated_message).await;
    let dkim_result = resolver.verify_dkim(&authenticated_message).await;

    // Build Authenticated-Results header
    let auth_results = AuthenticationResults::new("mx.mydomain.org")
        .with_dkim_result(&dkim_result, "[email protected]")
        .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.")
    }

SPF Policy Evaluation

    // Create a resolver using Cloudflare DNS
    let resolver = Resolver::new_cloudflare_tls().unwrap();

    // Verify HELO identity
    let result = resolver
        .verify_spf_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 = resolver
        .verify_spf_sender("::1".parse().unwrap(), "gmail.com", "my-local-domain.org", "[email protected]")
        .await;
    assert_eq!(result.result(), SpfResult::Fail);

DMARC Policy Evaluation

    // Create a resolver using Cloudflare DNS
    let resolver = Resolver::new_cloudflare_tls().unwrap();

    // Verify DKIM signatures
    let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap();
    let dkim_result = resolver.verify_dkim(&authenticated_message).await;

    // Verify SPF MAIL-FROM identity
    let spf_result = resolver
        .verify_spf_sender("::1".parse().unwrap(), "example.org", "my-local-domain.org", "[email protected]")
        .await;

    // Verify DMARC
    let dmarc_result = resolver
        .verify_dmarc(
            &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.

Testing & Fuzzing

To run the testsuite:

 $ cargo test

To fuzz the library with cargo-fuzz:

 $ cargo +nightly fuzz run mail_auth

Conformed RFCs

DKIM

SPF

DMARC

ARF

SMTP TLS Reporting

License

Licensed under either of

at your option.

Copyright

Copyright (C) 2020-2023, Stalwart Labs Ltd.

mail-auth's People

Contributors

djc avatar mdecimus avatar mtrnord avatar titussanchez avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

mail-auth's Issues

DMARC verify queries against root TLD

Using

[email protected]

The code will query _dmarc.corp -> I verified with Wireshark

Refs:

The code should probably query dmarc.domain.corp instead.
There is a few special TLDs on the links I mentioned

Some validation code (used in Laravel) explicitly has such a list hard coded: https://github.com/egulias/EmailValidator/blob/877577656387a3ffabae60dd4c425059179834cb/src/Validation/DNSCheckValidation.php#L23-L40

image

Correct API usage for long-lived programs

I was looking at how to use this library in the context of a long-lived application, specifically for sealing/signing emails, and am a bit confused as to what I should create once and reuse vs create at every operation.

My first thought was that I would need to import the RSA private key once at startup, pass it around, and instantiate/configure a sealer each time. However, RsaKey does not implement Clone, and ArcSealer::from_key() doesn't take a reference to a key, so that rules it out.

My second thought was that I would create an ArcSealer at startup and configure it when needed, since it appears to implement Clone. However, it depends, on both the State and the Signing Key being Clone, which they are not. So I could not configure the ArcSealer instance since I did not have a mutable reference.

My third attempt, and current one, is to create an ArcSealer and configure it at startup. Hence what I'm passing around is a Arc<ArcSealer<RsaKey<Sha256>, Done>>. Am I doing this right? :)

Thanks for the library!

Shouldn't dmarc results be fail if SPF or DKIM fail for a domain?

I'm currently testing this library and excuse me if this isn't correct but if SPF or DKIM fail for a message shouldn't the resulting output from verify_dmarc be a failure? Currently it looks like the default of None is returned.

Sorry if this isn't clear but I'm having some issues working through the logic of the dmarc verification and checking what's expected.

As an example the signature here is expired, but I think a similar thing happens if the SPF record is invalid (I change the IP address so it is a fail):

spf_result: SpfOutput { result: Pass, domain: "scsworld.co.uk", report: None, explanation: None }
dkim_result: [DkimOutput { result: Neutral(SignatureExpired), signature: ... x: 1699708744, t: 1699103944, r: false, atps: None, atpsh: None, ch: Relaxed, cb: Relaxed }), report: None, is_atps: false }]
dmarc_result: DmarcOutput { spf_result: Pass, dkim_result: None, domain: "scsworld.co.uk", policy: Reject, record: Some(Dmarc { v: V1, adkim: Relaxed, aspf: Relaxed, fo: Any, np: None, p: Reject, psd: Default, pct: 100, rf: 1, ri: 86400, rua: [URI { uri: ..., max_size: 0 }, URI ..., sp: None, t: false }) }

Shouldn't the output here actually be Fail not None?

mail-auth fails to build when using mail-send

Hi, just attempting a basic hello world mail-send, but compilation always fails on mail-auth:

Compiling mail-auth v0.3.7
error[E0599]: no method named unescape_value found for struct Attribute in the current scope
--> /Users/jim/.cargo/registry/src/index.crates.io-6f17d22bba15001f/mail-auth-0.3.7/src/report/dmarc/parse.rs:317:48
|
317 | if let Ok(attr) = attr.unescape_value() {
| ^^^^^^^^^^^^^^ help: there is a method with a similar name: decode_and_unescape_value

error[E0599]: no method named unescape_value found for struct Attribute in the current scope
--> /Users/jim/.cargo/registry/src/index.crates.io-6f17d22bba15001f/mail-auth-0.3.7/src/report/dmarc/parse.rs:322:48
|
322 | if let Ok(attr) = attr.unescape_value() {
| ^^^^^^^^^^^^^^ help: there is a method with a similar name: decode_and_unescape_value

For more information about this error, try rustc --explain E0599.
error: could not compile mail-auth (lib) due to 2 previous errors

my basic fn:

pub async fn email_report(machines: Vec<Machine>) {
    // Build a simple multipart message
    // More examples of how to build messages available at
    // https://github.com/stalwartlabs/mail-builder/tree/main/examples
    let message = MessageBuilder::new()
        .from(("someone", "[email protected]"))
        .to("other", "[email protected]")
        .subject("Hi!")
        .text_body("Hello world!");

    // Connect to the SMTP submissions port, upgrade to TLS and
    // authenticate using the provided credentials.
    SmtpClientBuilder::new("smtp.gmail.com", 587)
        .implicit_tls(false)
        .credentials(("someone", "p@ssw0rd"))
        .connect()
        .await
        .unwrap()
        .send(message)
        .await
        .unwrap();

}

my googling has not thrown anything useful up, hoping someone here can be more help, as I'd like to use these libraries.

*ring* crypto backend (and other feedback)

Hi, this crate is looking great, thanks for open sourcing! I was wondering, would you be open to maintaining alternative implementations for the required crypto primitives from ring, using an opt-in Cargo feature? In projects where rustls is already required, this saves on dependencies; it also looks like ed25519-dalek pulls in an obsolete version of rand.

Other minor points of feedback in random order:

  • I noticed that there are no rustfmt or clippy checks in the CI config even though the project is fmt/clippy clean, probably good to add?
  • Also acronyms in type names are not matching the recommended spelling from RFC 430 -- I can send a PR for this if you like. A potential sticking point is what ARC should be called, maybe Arc would be too confusing? I would suggest ArChain plus some docs, maybe?
  • is_within_pct() could probably use some comments on what it's trying to achieve and how.
  • In the DKIM crate I was working on that I'm considering replacing with this, I was writing out signatures etc into a fmt::Write instead of io::Write, which preserves the notion that the outcome is valid UTF-8. Maybe good to do that here, too? Also I would recommend not importing std::io::Write directly to avoid confusion between the two, I usually import std::fmt/std::io and then use io::Write/fmt::Write.
  • The fmt::Display impl for Error is inconsistent about using . at the end of strings -- I usually prefer to avoid them since it's easier/faster to add them later in surrounding context rather than the other way around.

I can file separate issues for these if you prefer.

wasm support

Hello, thanks for taking time to make these great libraries. Is wasm support something that's possible?

I was able to use mail-parser in a wasm environment, but haven't had the same luck for mail-auth.

Thank you!

MTA / MTA connection

Is mail-auth a Mail Transfer Agent?
Or how to connect mail-auth with a MTA like postfix?

This project's README.md doesn't tell.

Expired DKIM signatures should be a fail?

Hey, currently passing x=value where the time is passed to a DKIM signature will cause the signature to be marked as Neutral.

I don't think this is the right call. I could take an email that has been completely modified from the original (for example changing the from header) and instead of failing it will now be neutral. I understand that doing this skips the requirement of validation before it takes place because the signature has expired anyway but I think it might be better to either, 1> continue validation and pass neutral if the signature passes or 2> just go ahead and fail the signature as it's actually now invalid.

I'd be happy to put a pull request forward but it would be good to get a steer on your thinking first?

test feature

The design in 4383fcc seems a little surprising. What is the test feature intended to do? Is there a reason not to use #[cfg(test)] directly? It seems kinda strange that running the tests requires the test feature (otherwise a bunch of tests fail). Usually Cargo features should be additive, such that code compiles (and passes tests) whether the feature is enabled or not.

Requiring keys to be Clone

In my next step working on ring-based crypto impls, I'm running into an issue that I'd like your thoughts on before I go off in the wrong direction. In 3a571ed you added a Clone derive to the types that implement SigningKey for use in tests. However, ring's private key types generally don't implement Clone -- which is a kind of misuse-resistance that I value, making it harder to accidentally leak private material through a code base.

How do you think we should solve this? I could change ArcSealer/DkimSigner to take a Cow, which would allow the caller to decide whether they just want to just pass a reference instead of key ownership. Alternatively, I could change the tests to parse the key material once for every sealer/signer it wants to set up.

DKIM verify example is failing

I just tried the example in /examples/dkim_verify.rs and it fails with:

thread 'main' panicked at 'assertion failed: result.iter().all(|s| s.result() == &DkimResult::Pass)', examples/dkim_verify.rs:52:5

it seems that the header parsing code does not handle header values that span multiple lines. Was this working before?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.