sender check: From: read with OpenDKIM's own parser; L8 closed
test / test (push) Canceled after 0s

Audit 2026-10-12, stage 2c, the owner's decision of 2026-10-14: the check
decides by the address OpenDKIM signs for, whatever a display name or a
comment holds.

internal/milter/mailparse.go ports libopendkim's dkim_mail_parse() with
dkim_mail_first_special(), dkim_mail_token() and
dkim_mail_matching_paren() from Debian's opendkim 2.11.0~beta2-8+deb12u1,
line for line: the '<' form, group and list punctuation, an '@' before the
first ':', ';' or ',' deciding where the address starts, source routes
dropped, quoted strings and domain literals kept with their delimiters,
comments skipped, whitespace between atoms one space, no encoded word
decoded; and the C's own quirks ("@d" gives d as the local part too).
testdata/mailparse_harness.c compiles the C from the image's source; the
test feeds it and the port 200 019 lines (the hand-written cases and
200 000 random ones over the specials) - 93 443 parse and agree byte for
byte, 106 576 fail and agree on the error (on an error the C leaves its
in-place result unterminated, and OpenDKIM uses neither part then).

judge(): one From: field and one address stay required (moreAfter: only
list or group punctuation may follow); what OpenDKIM cannot read is
refused (mlfi_eoh() would not sign it); the address is lower-cased in
ASCII only, as dkimf_lowercase() and Postfix's tables do. Found reading
dkimf_apply_signtable(): OpenDKIM matches its refile SigningTable against
"user@domain" printed into MAXADDRESS + 1 = 257 bytes, so a longer address
is matched cut short - a quoted local part padded to end there in
"@another.tenant" would be signed with that tenant's key while its real
domain is the application's. Such an address (over 256 bytes) is refused;
one that fits is matched whole. The display-name and comment scan, the
"one @ outside quotes" rule and the SigningTable reader (Signers) go; the
check asks for header values with their leading space as OpenDKIM does
(OptHeaderLeadingSpace; Postfix keeps a v2 milter's event mask, milter8.c
milter8_negotiate()).

L8 closed: refused or passed no longer depends on other domains of the
server; security.md's accepted risk is struck. The shared other-sender
lines are now held to "never signed for the other sender": unit test (a
passed line is read at the application's domain) and e2e (a passed line
arrives signed for the application's domain, none for the other).
Documents: architecture "How the header is read", security's sender
check paragraph, the guide; the help topic per 2cf118c.

Stand (2026-10-15, make e2e, exit 0): sender_check_holds_the_from_line -
another sender's address in the display name now passes; a From: whose
quoted local part is padded so that, cut at 256 bytes, it ends in
"@other.e2e.test" is refused 550 5.7.1 "the address is longer than
OpenDKIM reads whole"; lines_naming_another_sender_never_signed_for_it
and passed_from_lines_signed_for_their_domain_only pass. go vet and go
test ./... pass on the server (24 packages); the differential test ran
there against the compiled C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-15 02:48:02 +03:00
co-authored by Claude Opus 5.5
parent 2cf118cafc
commit 3584adcbc2
16 changed files with 734 additions and 268 deletions
+11 -1
View File
@@ -852,7 +852,7 @@ guard against two parsers parting ways - and it is what makes L8 half
closed. This stage replaces the guard with the parser itself. Stage 3 and
those after it move up one patch number each.
- [ ] **Port `dkim_mail_parse` to Go, from the version the image runs**
- [x] **Port `dkim_mail_parse` to Go, from the version the image runs**
(Debian bookworm's opendkim 2.11.0~beta2 source package, not upstream's
develop branch), line for line, with its tokenizer
(`dkim_mail_first_special`, `dkim_mail_token`). The check then decides by
@@ -865,6 +865,16 @@ those after it move up one patch number each.
here (L8 closed; security.md's accepted risk struck). Whether the "one @
outside quotes" rule is still needed is decided against the port, and
said. - **Opus**
*Done 2026-10-15:* `internal/milter/mailparse.go`; the C itself compiled
from the image's source agrees with the port on 200 019 lines
(`testdata/mailparse_harness.c`). The "one @ outside quotes" rule goes:
the owner's decision says the address decides "whatever the display name
or a comment holds", and with OpenDKIM's parser there is no second one to
part ways with. Found reading the source: OpenDKIM matches its
SigningTable against "user@domain" cut to 256 bytes (MAXADDRESS), so a
longer address is refused - cut there, a quoted local part could end in
another tenant's domain. The check asks for header values with their
leading space, as OpenDKIM does; the SigningTable reader goes.
- [ ] **The port checked against OpenDKIM itself.** On the stand, several
hundred generated `From:` lines - quotes, escapes, comments nested and
open, groups, brackets, encoded words, a second domain of the server in