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:
+28
-33
@@ -215,28 +215,27 @@ operator can act on it.
|
||||
does not sign for included (the owner's rule: the header may say what the
|
||||
envelope may, nothing more). It is the first milter of the chain and
|
||||
strict: a message it could not judge is deferred, never passed on to the
|
||||
signer. It reads the header as one field holding one address and then
|
||||
looks at the raw value once more for any other name of OpenDKIM's
|
||||
`SigningTable`, so that a line its parser and OpenDKIM's could read
|
||||
differently is refused rather than argued about; outside quoted strings
|
||||
the line may hold one `@`. That the two agree is tested, not only argued:
|
||||
the e2e sends every `From:` line the check passes - a list it shares with
|
||||
the check's unit test, `internal/milter/testdata/from-lines-passed.txt` -
|
||||
and wants the message signed for the application's domain and no other;
|
||||
and every line of a second list, lines that name another sender of the
|
||||
server in a quoted string, a comment, a quoted pair, capitals or an encoded
|
||||
word (`from-lines-other.txt`), must be refused and never arrive signed for
|
||||
that sender. The reply to a refusal says that the line names another
|
||||
sender of the server, not which. That a line naming a domain is refused
|
||||
when the domain is a sender of this server, and passed when it is not,
|
||||
still tells an application which domains are hosted here; the probe needs
|
||||
an application of this server, and refusing every foreign address in a
|
||||
display name would change the 2.4.0 rule - accepted by the owner
|
||||
(accepted risks). What this does not promise is what a display
|
||||
name looks like to a person: a name of another sender written without an
|
||||
`@` the scan can find (a full-width dot, a space inside the name) passes,
|
||||
signed with the application's own key - a forged display name is as old
|
||||
as mail, and no key of another's is involved. What the client wrote
|
||||
signer. Since `2.5.2` it reads the header with OpenDKIM's own parser,
|
||||
`dkim_mail_parse()` ported line for line from the opendkim the image runs
|
||||
(`internal/milter/mailparse.go`), and judges the address that parser
|
||||
returns - the address OpenDKIM signs for - whatever a display name, a
|
||||
comment or a quoted local part holds; one `From:` field and one address in
|
||||
it stay required, and an address longer than the 256 bytes OpenDKIM
|
||||
matches its `SigningTable` against is refused, since cut there it could end
|
||||
in another domain's name (opendkim.c, `dkimf_apply_signtable()`,
|
||||
`MAXADDRESS`). That the port is OpenDKIM's parser is tested against the C
|
||||
itself, two hundred thousand lines and every result alike; that the signed
|
||||
domain follows, by the e2e: it sends every `From:` line the check passes (a
|
||||
list shared with the unit test,
|
||||
`internal/milter/testdata/from-lines-passed.txt`) and wants it signed for
|
||||
the application's domain and no other, and every
|
||||
line of a second list naming another sender of the server in a quoted
|
||||
string, a comment, a quoted pair, capitals or an encoded word
|
||||
(`from-lines-other.txt`) must be refused or arrive signed for the
|
||||
application's domain, never for that sender. What this does not promise is
|
||||
what a display name looks like to a person: a name of another sender in
|
||||
it passes, signed with the application's own key - a forged display name is
|
||||
as old as mail, and no key of another's is involved. What the client wrote
|
||||
reaches the reply, the log and the send log only as printable ASCII, cut,
|
||||
and a refused message is one row of the log whatever its recipient count.
|
||||
Mail the panel submits itself does not pass the check: only root,
|
||||
@@ -459,17 +458,13 @@ deferred item from the roadmap.
|
||||
through - but no message data. The inbound journal itself is in the
|
||||
database, 0640. Trigger to revisit: a shared host, where the sidecar should
|
||||
instead be given the files through a group it shares with the panel.
|
||||
- **An application can learn which domains are senders of this server.**
|
||||
A `From:` whose display name or comment names an address at a domain this
|
||||
server signs for is refused; one naming any other domain passes. The reply
|
||||
no longer names the domain (audit 2026-10-12, L8), but refused or passed
|
||||
answers the question for whoever sends the line. Accepted by the owner,
|
||||
2026-10-14: it needs an application of this server - a trusted tenant
|
||||
guessing names one at a time, as it already had to before L8, when the
|
||||
reply only said in words what the line had named - and closing it means
|
||||
refusing every foreign address in a display name, which would change his
|
||||
2.4.0 rule (a stranger's address in the name passes). Trigger to revisit:
|
||||
tenants who are not trusted, or a display-name rule decided anew.
|
||||
- ~~**An application can learn which domains are senders of this server.**~~
|
||||
Struck in `2.5.2` (audit 2026-10-12, stage 2c; L8 closed): the sender
|
||||
check reads a `From:` line with OpenDKIM's own parser, ported
|
||||
(`internal/milter/mailparse.go`), and decides by the address OpenDKIM signs
|
||||
for; what a display name, a comment or a quoted local part names no longer
|
||||
matters, so refused or passed says nothing about other domains of the
|
||||
server.
|
||||
- ~~**A row of the inbound log belongs to a domain by name.**~~ Struck in
|
||||
`2.5.0` (audit 2026-10-12, L12): both logs now hold a domain by id, see
|
||||
§ Roles.
|
||||
|
||||
Reference in New Issue
Block a user