Skip to content

fix(dkim): N+1 oversign present headers to detect prepended duplicates - #751

Merged
jiashuoz merged 2 commits into
mainfrom
fix/dkim-n-plus-one-oversigning
Jul 28, 2026
Merged

fix(dkim): N+1 oversign present headers to detect prepended duplicates#751
jiashuoz merged 2 commits into
mainfrom
fix/dkim-n-plus-one-oversigning

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

Summary

Listing each present header once in h= leaves header-duplication spoofing open. A DKIM verifier binds listed header instances from the bottom up, so a hop that prepends a second Subject still matches the original and reports pass — while the recipient's MUA displays the attacker's copy at the top.

Present headers are now listed n+1 times. The extra entry asserts "there is no further instance."

Raised during adversarial review of #746, which correctly noted the weakness predates that PR — the old static list also passed each candidate once.

Confirmed both ways against the signer

Not argued from the RFC — measured:

h= listing Prepended spoof Subject: YOU HAVE WON
From:Subject (before) verifies clean — spoof succeeds
From:From:Subject:Subject (after) fails — spoof detected

Same result for From, To, Reply-To, and Message-ID.

Mutation-verified in reverse too: reverting to single listing fails TestSign_PrependedDuplicateHeaderIsDetected with "a prepended duplicate Subject went undetected" on every case.

This must not undo #746

The two interact directly, so there's a dedicated test for it. Absent candidates are still omitted from h= entirely — that's what keeps SES stamping its own Message-ID from breaking the signature. N+1 applies only to headers the message actually carries.

TestSign_NPlusOneDoesNotResurrectOversigning asserts the composer-shaped message still verifies after SES adds Message-ID, so this hardening cannot silently reintroduce the bug it builds on.

Trade-off

A relay that legitimately adds a duplicate of a header we already set would now break the signature. Accepted: no relay in this path does so, and the candidate list is narrow. The alternative is silently accepting a spoofed display value, which is the worse failure — a signature that says "verified" over content the user never sees.

Client surface checklist

Not applicable — internal signing behavior. No API surface, schema, or migration; make generate untouched.

Merge order

Builds on #746 (fix/dkim-sign-present-headers) and is branched from it, since it modifies the function that PR introduces. Merge after #746.

Test plan

  • go test ./internal/dkim/ — green
  • make test-unit — full unit suite, 0 failures
  • gofmt / go vet clean
  • Prepended-duplicate detection across Subject, From, To, Reply-To, Message-ID, with an unmutated control asserted first
  • signedHeaderKeys table extended: present headers listed n+1, an already-duplicated header listed 3 times, absent From still listed exactly once per RFC 6376 § 5.4
  • Mutation-verified: reverting to single listing fails every spoof case

🤖 Generated with Claude Code

jiashuoz and others added 2 commits July 27, 2026 22:48
Listing each present header once in h= leaves header-duplication
spoofing open. A verifier binds listed instances from the bottom up, so
a hop that PREPENDS a second Subject still matches the original and
reports pass - while the MUA displays the attacker's copy at the top.

Confirmed both ways against the signer. With Subject listed once, a
prepended spoof verifies clean; listed twice, it fails. The same holds
for From, To, Reply-To and Message-ID.

Present headers are now listed n+1 times, so the extra entry asserts
"there is no further instance". Absent candidates are still omitted
entirely, which is what keeps SES stamping its own Message-ID from
breaking the signature - a dedicated test pins that this hardening does
not resurrect the oversigning bug it builds on.

Trade-off: a relay that legitimately adds a duplicate of a header we
already set would now break the signature. No relay in this path does
so, and the candidate list is narrow. That is the intended direction:
the alternative is silently accepting a spoofed display value.

Builds on the h= narrowing; should merge after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jiashuoz
jiashuoz force-pushed the fix/dkim-n-plus-one-oversigning branch from 453f4c8 to dfc2e5e Compare July 28, 2026 05:49
@jiashuoz
jiashuoz merged commit 438e343 into main Jul 28, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant