L8: a From: line naming another sender of the server was refused with
"it names another sender of this server, <domain>" - a confirmation, to
whoever tries names, that the domain is hosted here. The reason now stops
before the domain; the line as written stays in the reply (the client's own
text) and on the refused message's page (header_from). Unit test: the reason
of every refusal names none of the server's other domains; e2e: the reason
of each sender-check refusal does not contain other.e2e.test.
Which key OpenDKIM signs with (owner, 2026-10-13): the check and OpenDKIM
read From: with two parsers, and their agreement was argued, never run.
internal/milter/testdata/from-lines-passed.txt holds the lines the check
passes for an application - bare and bracketed, quoted names with a comma,
escaped quotes and an address inside, an extension, three encoded charsets,
two folded lines, comments (nested too), a stranger's address in the name -
as Go-quoted strings with {domain}. The unit test wants each passed; the new
e2e case passed_from_lines_signed_for_their_domain_only sends each as app1
and wants every DKIM signature at the sink to be for the sender domain,
with one valid. All fifteen were. "(the shop) a@domain" - a comment before
the address, valid RFC 5322 - is refused by the check ("it must be one
address"); strict, not a hole, so it is not in the file. Groups are refused
too.
Audit plan lines ticked (two) with what was found, CHANGELOG [Unreleased],
roadmap 21/62.
Evidence on the test server for this tree (with a618ffb): go vet clean, go
test ./... 23 packages ok, make e2e exit 0 - the new case passed in 4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/emersion/go-msgauth/dkim"
|
|
)
|
|
|
|
// passedFromLines reads the From: lines the sender check passes, the file its
|
|
// unit test reads too, with the application's domain put in.
|
|
func passedFromLines(repoRoot, domain string) ([]string, error) {
|
|
raw, err := os.ReadFile(filepath.Join(repoRoot, "internal", "milter", "testdata", "from-lines-passed.txt"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
s, err := strconv.Unquote(line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", line, err)
|
|
}
|
|
out = append(out, strings.ReplaceAll(s, "{domain}", domain))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// testSignedForItsDomainOnly: the sender check and OpenDKIM read From: with
|
|
// two parsers, and that they agree was argued, never run (audit 2026-10-12,
|
|
// stage 2). Every From: line the check passes for an application is sent as
|
|
// that application, and what arrives must be signed for its domain - with a
|
|
// signature that validates - and for no other: a line the check reads as the
|
|
// application's own and OpenDKIM as another sender's would be signed with
|
|
// that sender's key.
|
|
func testSignedForItsDomainOnly(t *testing.T, sc *scenario) {
|
|
lines, err := passedFromLines(h.repoRoot, senderDomain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, line := range lines {
|
|
token := uniqueToken("dkimfrom")
|
|
res := attemptSend(sendAttempt{
|
|
authLogin: sc.appLogin, authPassword: sc.appPassword,
|
|
from: "alerts@" + senderDomain, headerFrom: line, to: recipient,
|
|
subject: "e2e " + token, body: "which key signs this",
|
|
})
|
|
if !res.ok() {
|
|
t.Errorf("From: %q was refused: %v", line, res.firstErr())
|
|
continue
|
|
}
|
|
raw, err := findSinkMessage(h.stageDir, token, 30*time.Second)
|
|
if err != nil {
|
|
t.Errorf("From: %q never reached the sink: %v", line, err)
|
|
continue
|
|
}
|
|
verifications, err := dkim.VerifyWithOptions(bytes.NewReader(raw), &dkim.VerifyOptions{LookupTXT: lookupTXTFunc()})
|
|
if err != nil {
|
|
t.Errorf("From: %q: dkim verify: %v", line, err)
|
|
continue
|
|
}
|
|
valid := false
|
|
for _, v := range verifications {
|
|
if v.Domain != senderDomain {
|
|
t.Errorf("From: %q was signed for %s", line, v.Domain)
|
|
}
|
|
if v.Domain == senderDomain && v.Err == nil {
|
|
valid = true
|
|
}
|
|
}
|
|
if !valid {
|
|
t.Errorf("From: %q has no valid signature for %s (%d signature(s))", line, senderDomain, len(verifications))
|
|
}
|
|
}
|
|
}
|