InboxGrade

DKIM canonicalization issue (simple vs relaxed)

The DKIM signature was generated with `simple` canonicalization, so any whitespace or header change by a relaying MTA invalidates the signature.

Impact — Medium impact

simple/simple canonicalization requires the message to arrive at the verifier byte-for-byte identical to how it was signed. In practice, mailing lists, forwarders, SMTP gateways, and some anti-spam appliances rewrite headers, fold long lines, or strip trailing whitespace, which breaks the body hash or header hash and produces dkim=fail. The result is intermittent DKIM failures that only appear on certain paths, which cascades into DMARC failures and spam folder placement for a subset of recipients.

What causes it

  1. Signing software defaults to c=simple/simple (older OpenDKIM, legacy Sendmail/milter configs, and some appliance defaults) instead of the far more tolerant c=relaxed/relaxed.
  2. A mailing list manager (Mailman, Google Groups, listserv) folds, wraps, or adds List-* headers, altering the exact byte stream the simple header canonicalization requires.
  3. An outbound relay or security gateway (Mimecast, Proofpoint, Barracuda) rewrites headers, normalizes line endings, or adds a disclaimer, invalidating simple body canonicalization.
  4. A forwarding server converts CRLF to LF (or vice versa), or strips trailing whitespace from body lines — tolerated under relaxed but fatal under simple.
  5. Content filters or DLP engines inject or modify headers (e.g. X- tags, Received-SPF) between the signer and the verifier, breaking header canonicalization when the signer included those headers in h=.

How to fix it

Confirm which canonicalization is actually in use

Inspect the DKIM-Signature header on a received message and look at the c= tag. c=simple/simple (or an omitted c=, which defaults to simple/simple) is the brittle case; c=relaxed/relaxed is what you want.

# Pull a recent message from your mailbox and inspect the signature
grep -i '^DKIM-Signature' /path/to/raw-message.eml | tr ';' '\n' | grep -i 'c='

Switch the signer to relaxed/relaxed

In OpenDKIM, set Canonicalization relaxed/relaxed in /etc/opendkim.conf and reload. In Postfix with dkimpy-milter, set Canonicalization relaxed/relaxed. For application-level signers (Node nodemailer, Python dkimpy, PHP PHPMailer), pass the canonicalization option explicitly rather than relying on library defaults.

# /etc/opendkim.conf
Canonicalization   relaxed/relaxed
SignatureAlgorithm rsa-sha256

# Then:
systemctl reload opendkim

Re-sign after any downstream content modification

If a gateway or DLP must modify messages after signing, move the DKIM signer to the final hop so the signature is applied to the exact bytes that leave your network. Signing before a relay that rewrites content guarantees failure regardless of canonicalization.

Validate the fix against a real receiver

Send a test message through the full production path (including any list, gateway, or forwarder) and verify dkim=pass in the receiving server's Authentication-Results header. Do not trust a loopback test — the breakage is specifically about what intermediaries do in transit.

# On the receiving MTA, inspect the delivered message
grep -i 'Authentication-Results' /var/mail/user | head -5

Fixed it? Run a fresh test to confirm.

Test my inbox →

Related errors