Both RSA and Ed25519 fail, Win25

Issue Description

Summary

On a fresh Stalwart v0.16.18 installation, outbound messages are signed with both the automatically generated RSA and Ed25519 DKIM signatures, but Gmail reports both signatures as failed.

The problem does not appear to be DNS publication, message mutation, SPF, source-IP selection, or body canonicalization.

Environment

  • Stalwart: v0.16.18
  • Operating system: Windows Server 2025 Standard
  • Database: MySQL 8.4.11
  • Blob storage: local filesystem
  • Directory: internal
  • Installation: fresh setup using the Stalwart WebUI wizard
  • DKIM keys: automatically generated by Stalwart during setup
  • Outbound delivery: direct to recipient MX, no SMTP relay
  • Test recipient: Gmail

Domain and selectors

Domain:

mailoak.com

Selectors:

v1-ed25519-20260819

v1-rsa-20260819

The Ed25519 public key shown by Stalwart under the DKIM signature object is:

SJuRaRsbyGkh4lKo//AA5zRjMWcIVpcYm/nkNN2/Dk4=

The public DNS record contains the exact same key:

v=DKIM1; k=ed25519; h=sha256; p=SJuRaRsbyGkh4lKo//AA5zRjMWcIVpcYm/nkNN2/Dk4=

The RSA public key shown/generated by Stalwart was likewise published directly in DNS.

Both records resolve publicly from Google and Cloudflare DNS.

Reproduction

  1. Install Stalwart v0.16.18 on Windows Server 2025.
  2. Configure a local domain through the setup wizard.
  3. Enable automatic DKIM key generation.
  4. Publish the generated RSA and Ed25519 public keys in DNS.
  5. Create a normal user account.
  6. Submit a message through Stalwart’s authenticated SMTPS listener on port 465.
  7. Allow Stalwart to deliver directly to Gmail.
  8. Inspect Gmail’s “Show original” output.

Expected result

At least the RSA DKIM signature, and ideally both signatures, should verify successfully.

Actual result

Gmail reports:

dkim=fail [email protected] header.s=v1-ed25519-20260819
dkim=fail [email protected] header.s=v1-rsa-20260819
spf=pass
dmarc=pass

SPF passes using the intended dedicated sending IP:

51.83.169.75

DMARC passes through aligned SPF.

Relevant received headers

Authentication-Results: mx.google.com;
       dkim=fail [email protected] header.s=v1-ed25519-20260819;
       dkim=fail [email protected] header.s=v1-rsa-20260819;
       spf=pass (google.com: domain of [email protected] designates 51.83.169.75 as permitted sender) [email protected];
       dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=mailoak.com

The message was received directly from the intended Stalwart host:

Received: from smtp.mailoak.com (smtp.mailoak.com. [51.83.169.75])
        by mx.google.com with ESMTPS
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256)

Stalwart emitted both signatures:

DKIM-Signature: v=1; a=ed25519-sha256;
 s=v1-ed25519-20260819;
 d=mailoak.com;
 c=relaxed/relaxed;
 h=Subject:Message-ID:Date:To:From;
 bh=ymp4mDy0dOH0P1M4+xU9vVowuXejoGXdc3J+b5JDfYM=;
 ...

DKIM-Signature: v=1; a=rsa-sha256;
 s=v1-rsa-20260819;
 d=mailoak.com;
 c=relaxed/relaxed;
 h=Subject:Message-ID:Date:To:From;
 bh=ymp4mDy0dOH0P1M4+xU9vVowuXejoGXdc3J+b5JDfYM=;
 ...

Verification already performed

Body hash

The received message body was independently canonicalized using relaxed DKIM body canonicalization and SHA-256.

The calculated body hash is:

ymp4mDy0dOH0P1M4+xU9vVowuXejoGXdc3J+b5JDfYM=

This exactly matches the bh= value in both Stalwart-generated DKIM signatures.

This appears to rule out message-body mutation after signing.

Public key

For the Ed25519 signer, the public key shown by Stalwart as derived from the stored private key is byte-for-byte identical to the public key published in DNS:

SJuRaRsbyGkh4lKo//AA5zRjMWcIVpcYm/nkNN2/Dk4=

Therefore this does not appear to be the public/private key mismatch described in issue #3188.

Independent signature verification

The Ed25519 b= signature from the Gmail-received message was independently verified against that exact public key and did not validate.

The same verifier was first tested against the RFC 8463 Ed25519 DKIM test vector, which validated correctly.

Other mail-path checks

  • Forward DNS is correct.
  • PTR matches smtp.mailoak.com.
  • SPF passes.
  • Outbound source IP is explicitly bound to 51.83.169.75.
  • TLS works correctly.
  • Gmail receives the message directly from the expected IP.
  • No intermediary SMTP relay is involved.
  • DMARC passes via SPF alignment.

Related issues

This appears related to, but distinct from:

  • #3193 - Ed25519 DKIM fails while RSA passes.
  • #3188 - RSA private/public key mismatch after a setup-wizard race.

In this case:

  • both RSA and Ed25519 fail
  • the Ed25519 public key derived by Stalwart matches DNS exactly
  • the DKIM body hash matches the received body
  • the setup completed successfully
  • there is no third-party relay modifying the message

Additional evidence

A raw Gmail .eml containing both DKIM signatures and Gmail’s Authentication-Results is available if useful.

Happy to run a more targeted trace or provide canonicalized header bytes if maintainers need them for reproduction.

Stalwart Version

v0.16.x

Installation Method

Binary (Windows)

Database Backend

MySQL

Blob Storage

Filesystem

Search Engine

MySQL

Directory Backend

Internal

Additional Context

wrapped the executable with NSSM as the Windows service

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

Additional reproduction: The same issue reproduces on a second independently configured domain (norai.fi) using a completely new automatically generated RSA and Ed25519 keypair. Both signatures fail verification at Gmail, while SPF passes from 51.83.169.75 and DMARC passes through SPF alignment. This appears to rule out a domain-specific DNS or original-keypair issue.

The body hash matching plus a derived public key rules out both of the issues you linked, so the mismatch has to be in the header hash. I have been through the signing path and cannot find a way for it to diverge: what gets signed and what gets written to the queue are built from the same bytes in the same order, the signature header is hashed and serialised by one function so the tag order cannot differ, and every header in your h= came from your own submission rather than anything we prepend.

That leaves the two inputs that change a signature without touching bh=: t= and x=. You elided those in the paste. Could you send the raw .eml you offered, with the complete signature headers including t= and any x= or l=? And separately, what is the system clock and timezone on that Windows Server box? A skewed clock under NSSM would produce exactly this: correct body hash, failed signature.

Two other things to check: whether the domain has more than one DKIM key object per algorithm (Settings > Domains > DKIM), since duplicates would show up as two signatures sharing a selector, and whether any Sieve script or milter runs on the submission path, since those can replace the message before signing.

Thanks - I checked the remaining points.

I’ve attached the raw .eml from the original Mailoak delivery test with the complete DKIM headers intact.

For that message:

Ed25519:
t=1787166682

RSA:
t=1787166682

There is no x= and no l= in either signature. The timestamp corresponds to 2026-08-19 19:11:22 UTC, which also matches the message Date: header exactly. The full signatures, including h=, bh= and b=, are present in the attached message.

I also checked the Windows Server clock/time configuration. It appears healthy and synchronised:

Local time:
2026-08-24T15:49:13.4722366+03:00

UTC:
2026-08-24T12:49:13.4762516Z

Timezone:
FLE Standard Time
UTC+03:00 currently

Windows Time:
Leap Indicator: 0 (no warning)
Stratum: 5
Source: time.windows.com,0x8
Last Successful Sync Time: 8/24/2026 15:47:52

So I don’t see evidence of meaningful clock skew.

I also checked the DKIM objects in the Stalwart UI. There are exactly six signatures total across three domains - one RSA and one Ed25519 object per domain - with unique selectors. For mailoak.com specifically there is only:

v1-rsa-20260819
v1-ed25519-20260819

so there are no duplicate DKIM objects sharing the selector.

The same dual-signature failure has also reproduced on the independently configured norai.fi keypair as noted earlier.

I’ll check the Sieve/Milter side separately as well, but at this point the clock and duplicate-key possibilities both appear ruled out.

The attached raw message is the cleaner standalone delivery test rather than the Thunderbird reply sample, so there should be less unrelated message-path noise in it.

Download .eml:

http://mailoak.com/lib/delivery_test.zip

Clock, duplicate keys and the t=/x= question are all answered, so let’s retire those. What I want now is to reproduce this offline from the .eml you attached.

Run it through a verifier that names the failure mode, since Gmail’s bare dkim=fail tells us nothing useful: dkimverify from dkimpy, or opendkim-testmsg. If it fails locally against your published key as well, the fault is on the signing side and reproducible, and I can work from there. If it verifies locally, something is altering a signed header between our queue and Gmail, which is a different hunt entirely.

The other thing I would check is were the keys on either domain rotated or re-created after the wizard ran, and has the server been restarted since? Signers are cached per domain, and a rotation without a restart is the one path I can see where the key doing the signing would differ from the one the WebUI derives and shows you.

I tested the attached .eml locally using dkimpy 1.1.8 with PyNaCl installed:

dkimverify --index 0 - signature ok
dkimverify --index 1 - signature ok

Both signatures therefore verify locally against the currently published DNS keys, although Gmail reported both as failed. What would you recommend checking next?

I have also restarted the server multiple times. Stalwart generated the keys, and I have not rotated, recreated or manually modified them afterwards.

I sent a new message to an OVH-hosted mailbox. OVH also rejects both DKIM signatures:

ed25519: public key: missing p= tag
rsa: public key: syntax error

SPF and DMARC pass. Both signatures still return signature ok locally with dkimpy.

This therefore does not appear to be Gmail-specific. Two independent receivers reject the published public-key records, while dkimpy accepts them. Could you inspect the exact DNS TXT syntax generated for these two selectors and explain why OVH and Gmail cannot parse it?

Quick update: the original dual DKIM failure is no longer reproducible.

While investigating a separate WordPress delivery failure today, I found that PHP was configured to use a CA bundle that had never been installed:

C:\php\certs\cacert.pem

This prevented PHP from establishing the TLS connection to Stalwart on smtp.mailoak.com:465. I installed a current CA bundle, corrected the PHP 8.5.9 paths, restarted IIS and confirmed that WordPress could submit mail successfully through Stalwart again.

That PHP issue was separate from DKIM validation, but it gave me an opportunity to run a completely fresh end-to-end delivery test through Stalwart.

The new message was delivered directly from smtp.mailoak.com at 51.83.169.75 to Gmail in approximately five seconds over TLS 1.3. Gmail now reports:

spf=pass [email protected]

dkim=pass [email protected] header.s=v1-rsa-20260820

dkim=neutral (no key) [email protected] header.s=v1-ed25519-20260820

dmarc=pass header.from=norai.fi

The overall Gmail result is therefore:

  • SPF: PASS

  • DKIM: PASS

  • DMARC: PASS

I did not rotate or recreate the DKIM keys, change the Stalwart signer configuration or restart Stalwart as part of today’s PHP repair. The existing RSA signature now validates successfully at Gmail, so the earlier condition in which both RSA and Ed25519 were rejected has disappeared.

The Ed25519 signature is still reported as neutral (no key), so I am not claiming that part is resolved. However, the RSA signature now passes correctly and provides aligned DKIM authentication for DMARC.

In short, authenticated submission had a separate PHP CA-bundle problem, which is now fixed, and a fresh delivery confirms that Stalwart’s RSA DKIM signature is currently accepted by Gmail. The precise reason the earlier messages produced dual DKIM failures remains unclear, but I can no longer reproduce that result.