Ed25519 DKIM signatures fail verification at Gmail while RSA signatures on the same message pass

Issue Description

Per-domain Ed25519 DKIM signatures generated by Stalwart consistently fail verification at Gmail while the RSA signature on the very same message verifies cleanly. The published Ed25519 public key in DNS matches the public key stored in the server’s DkimSignature object byte-for-byte, so this is not a DNS publish issue.

DMARC overall still passes thanks to the RSA signature, so this is currently cosmetic — but it produces a steady dkim=fail line in every DMARC aggregate report Google returns, across every domain we run.

Previously filed as GitHub issue #3193 (auto-closed by the triage bot with a request to repost here).

Expected Behavior

Both DKIM signatures Stalwart emits for a message (Ed25519 and RSA) should verify against their respective published public keys at the receiver. RFC 8463 §3 says verifiers MUST treat valid Ed25519 signatures as authoritative, and the published Ed25519 key in DNS already matches the server’s stored key.

Actual Behavior

For the same outbound message: - RSA selector v1-rsa-20260601 → dkim=pass at Gmail - Ed25519 selector v1-ed25519-20260601 → dkim=fail at Gmail

Same canonicalisation (relaxed/relaxed), same signed headers (From, To, Date, Subject, Message-ID), same body. Reproduced across 7 different tenant domains; Ed25519 selectors were created independently between 2026-05-21 and 2026-06-03, every one of them fails the same way.

Sample DMARC aggregate fragment from Google:

xml <auth_results> jabali-panel.com fail v1-ed25519-20260601 jabali-panel.com pass v1-rsa-20260601 jabali-panel.com pass </auth_results>

Relevant Log Output

Add any new domain in Stalwart on a host with a public IP. Stalwart auto-generates one Ed25519 + one RSA DkimSignature for it.
Publish both DKIM TXT records (and the matching SPF + DMARC) at the DNS provider.
Send mail from @ to any Gmail address.
Wait for the daily DMARC aggregate report from [email protected], or inspect the Authentication-Results header on the delivered Gmail message.
Observed: dkim=fail for the Ed25519 selector, dkim=pass for the RSA selector, on the same message.
Relevant Log Output
No relevant errors in the Stalwart log — signing succeeds without warning, both signatures are emitted, and the message is accepted by Gmail (DMARC=pass overall via the RSA signature). The failure is visible only on the receiver side via the DMARC aggregate report shown above.

Verified pieces on our end:

Check Value
DNS pubkey at v1-ed25519-20260601._domainkey.jabali-panel.com v=DKIM1; k=ed25519; p=TSI1vcJ8BNeVHc4ArJ7aJs7f+sCs/CgukmF3S84t5Ws=
Stalwart DkimSignature.publicKey for that selector TSI1vcJ8BNeVHc4ArJ7aJs7f+sCs/CgukmF3S84t5Ws= (byte-identical)
@type Dkim1Ed25519Sha256
canonicalization relaxed/relaxed
headers From, To, Date, Subject, Message-ID
Source IP server itself (no third-party relay)
Independent Ed25519 keys across multiple domains and dates all fail identically — rules out single-key bit-rot
I’m happy to capture an outbound message via a one-shot MtaRoute to a local netcat listener and attach the full signed payload + canonicalised body bytes for a deterministic local repro — let me know if that would help and I’ll post it as a follow-up.

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

7 tenant domains, every one shows the behaviour.
No upstream relay; outbound goes directly from Stalwart to recipient MX.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Gmail does not support ed25519, that is why Stalwart signs using both RSA and Ed.

You were right — apologies for the noise. I went back and ran the actual cryptographic verification locally instead of trusting Google’s fail line in the DMARC report.

I captured an outbound message signed by my Stalwart instance (selector riva, algorithm Ed25519, domain reeva.me) by submitting it through SMTP authenticated submission and pulling it back via IMAP from the recipient’s mailbox. Then ran dkimpy against it, with dkimpy resolving the published _domainkey TXT itself:

DEBUG:dkimpy:sig: {b'v': b'1', b'a': b'ed25519-sha256', b's': b'riva', b'd': b'reeva.me', b'c': b'relaxed/relaxed', ...} DEBUG:dkimpy:bh: b'Zr5kg0noYypax4XKXgqFG1CU+zVU0hmTawnzT+UUc4w=' DEBUG:dkimpy:b'DKIM-Signature' valid sig[ed25519] verified = True

So Stalwart’s Ed25519 signing pipeline is mathematically correct — body hash and signature both validate against the published key. The fail line in Google’s DMARC aggregate report is misleading on their side: RFC 8463 §3 says unrecognized algorithms MUST be treated as if the signature did not exist, but their reporter emits fail instead of none. So this looked from the outside like a verification failure when it was actually “we don’t (yet) verify this algorithm”.

For anyone landing here from a search with the same symptoms: it’s purely cosmetic — RSA covers DMARC at Gmail. If you want to clean up the reports, deleting the per-domain Dkim1Ed25519Sha256 row via stalwart-cli delete DkimSignature --ids … is the trivial fix. I’ve also wired a dropEd25519Sigs step into our own EnsureDomain flow so new domains don’t accumulate them. No upstream change needed — feel free to close this with whichever button is appropriate.