mirror of
https://github.com/mjl-/mox.git
synced 2025-07-10 09:54:40 +03:00
mox!
This commit is contained in:
275
dmarc/dmarc_test.go
Normal file
275
dmarc/dmarc_test.go
Normal file
@ -0,0 +1,275 @@
|
||||
package dmarc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/dkim"
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/spf"
|
||||
)
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
resolver := dns.MockResolver{
|
||||
TXT: map[string][]string{
|
||||
"_dmarc.simple.example.": {"v=DMARC1; p=none;"},
|
||||
"_dmarc.one.example.": {"v=DMARC1; p=none;", "other"},
|
||||
"_dmarc.temperror.example.": {"v=DMARC1; p=none;"},
|
||||
"_dmarc.multiple.example.": {"v=DMARC1; p=none;", "v=DMARC1; p=none;"},
|
||||
"_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus;"},
|
||||
"_dmarc.example.com.": {"v=DMARC1; p=none;"},
|
||||
},
|
||||
Fail: map[dns.Mockreq]struct{}{
|
||||
{Type: "txt", Name: "_dmarc.temperror.example."}: {},
|
||||
},
|
||||
}
|
||||
|
||||
test := func(d string, expStatus Status, expDomain string, expRecord *Record, expErr error) {
|
||||
t.Helper()
|
||||
|
||||
status, dom, record, _, err := Lookup(context.Background(), resolver, dns.Domain{ASCII: d})
|
||||
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
|
||||
t.Fatalf("got err %#v, expected %#v", err, expErr)
|
||||
}
|
||||
expd := dns.Domain{ASCII: expDomain}
|
||||
if status != expStatus || dom != expd || !reflect.DeepEqual(record, expRecord) {
|
||||
t.Fatalf("got status %v, dom %v, record %#v, expected %v %v %#v", status, dom, record, expStatus, expDomain, expRecord)
|
||||
}
|
||||
}
|
||||
|
||||
r := DefaultRecord
|
||||
r.Policy = PolicyNone
|
||||
test("simple.example", StatusNone, "simple.example", &r, nil)
|
||||
test("one.example", StatusNone, "one.example", &r, nil)
|
||||
test("absent.example", StatusNone, "absent.example", nil, ErrNoRecord)
|
||||
test("multiple.example", StatusNone, "multiple.example", nil, ErrMultipleRecords)
|
||||
test("malformed.example", StatusPermerror, "malformed.example", nil, ErrSyntax)
|
||||
test("temperror.example", StatusTemperror, "temperror.example", nil, ErrDNS)
|
||||
test("sub.example.com", StatusNone, "example.com", &r, nil) // Policy published at organizational domain, public suffix.
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
resolver := dns.MockResolver{
|
||||
TXT: map[string][]string{
|
||||
"_dmarc.reject.example.": {"v=DMARC1; p=reject"},
|
||||
"_dmarc.strict.example.": {"v=DMARC1; p=reject; adkim=s; aspf=s"},
|
||||
"_dmarc.test.example.": {"v=DMARC1; p=reject; pct=0"},
|
||||
"_dmarc.subnone.example.": {"v=DMARC1; p=reject; sp=none"},
|
||||
"_dmarc.none.example.": {"v=DMARC1; p=none"},
|
||||
"_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus"},
|
||||
"_dmarc.example.com.": {"v=DMARC1; p=reject"},
|
||||
},
|
||||
Fail: map[dns.Mockreq]struct{}{
|
||||
{Type: "txt", Name: "_dmarc.temperror.example."}: {},
|
||||
},
|
||||
}
|
||||
|
||||
equalResult := func(got, exp Result) bool {
|
||||
if reflect.DeepEqual(got, exp) {
|
||||
return true
|
||||
}
|
||||
if got.Err != nil && exp.Err != nil && (got.Err == exp.Err || errors.Is(got.Err, exp.Err)) {
|
||||
got.Err = nil
|
||||
exp.Err = nil
|
||||
return reflect.DeepEqual(got, exp)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
test := func(fromDom string, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, expUseResult bool, expResult Result) {
|
||||
t.Helper()
|
||||
|
||||
from, err := dns.ParseDomain(fromDom)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing domain: %v", err)
|
||||
}
|
||||
useResult, result := Verify(context.Background(), resolver, from, dkimResults, spfResult, spfIdentity, true)
|
||||
if useResult != expUseResult || !equalResult(result, expResult) {
|
||||
t.Fatalf("verify: got useResult %v, result %#v, expected %v %#v", useResult, result, expUseResult, expResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic case, reject policy and no dkim or spf results.
|
||||
reject := DefaultRecord
|
||||
reject.Policy = PolicyReject
|
||||
test("reject.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
true, Result{true, StatusFail, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// Accept with spf pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusPass,
|
||||
&dns.Domain{ASCII: "sub.reject.example"},
|
||||
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// Accept with dkim pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusPass,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "sub.reject.example"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusFail,
|
||||
&dns.Domain{ASCII: "reject.example"},
|
||||
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// Reject due to spf and dkim "strict".
|
||||
strict := DefaultRecord
|
||||
strict.Policy = PolicyReject
|
||||
strict.ADKIM = AlignStrict
|
||||
strict.ASPF = AlignStrict
|
||||
test("strict.example",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusPass,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "sub.strict.example"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusPass,
|
||||
&dns.Domain{ASCII: "sub.strict.example"},
|
||||
true, Result{true, StatusFail, dns.Domain{ASCII: "strict.example"}, &strict, nil},
|
||||
)
|
||||
|
||||
// No dmarc policy, nothing to say.
|
||||
test("absent.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, ErrNoRecord},
|
||||
)
|
||||
|
||||
// No dmarc policy, spf pass does nothing.
|
||||
test("absent.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusPass,
|
||||
&dns.Domain{ASCII: "absent.example"},
|
||||
false, Result{false, StatusNone, dns.Domain{ASCII: "absent.example"}, nil, ErrNoRecord},
|
||||
)
|
||||
|
||||
none := DefaultRecord
|
||||
none.Policy = PolicyNone
|
||||
// Policy none results in no reject.
|
||||
test("none.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusPass,
|
||||
&dns.Domain{ASCII: "none.example"},
|
||||
true, Result{false, StatusPass, dns.Domain{ASCII: "none.example"}, &none, nil},
|
||||
)
|
||||
|
||||
// No actual reject due to pct=0.
|
||||
testr := DefaultRecord
|
||||
testr.Policy = PolicyReject
|
||||
testr.Percentage = 0
|
||||
test("test.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
false, Result{true, StatusFail, dns.Domain{ASCII: "test.example"}, &testr, nil},
|
||||
)
|
||||
|
||||
// No reject if subdomain has "none" policy.
|
||||
sub := DefaultRecord
|
||||
sub.Policy = PolicyReject
|
||||
sub.SubdomainPolicy = PolicyNone
|
||||
test("sub.subnone.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusFail,
|
||||
&dns.Domain{ASCII: "sub.subnone.example"},
|
||||
true, Result{false, StatusFail, dns.Domain{ASCII: "subnone.example"}, &sub, nil},
|
||||
)
|
||||
|
||||
// No reject if spf temperror and no other pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusTemperror,
|
||||
&dns.Domain{ASCII: "mail.reject.example"},
|
||||
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// No reject if dkim temperror and no other pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusTemperror,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "sub.reject.example"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
true, Result{false, StatusTemperror, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// No reject if spf temperror but still dkim pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusPass,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "sub.reject.example"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusTemperror,
|
||||
&dns.Domain{ASCII: "mail.reject.example"},
|
||||
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// No reject if dkim temperror but still spf pass.
|
||||
test("reject.example",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusTemperror,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "sub.reject.example"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusPass,
|
||||
&dns.Domain{ASCII: "mail.reject.example"},
|
||||
true, Result{false, StatusPass, dns.Domain{ASCII: "reject.example"}, &reject, nil},
|
||||
)
|
||||
|
||||
// Bad DMARC record results in permerror without reject.
|
||||
test("malformed.example",
|
||||
[]dkim.Result{},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
false, Result{false, StatusPermerror, dns.Domain{ASCII: "malformed.example"}, nil, ErrSyntax},
|
||||
)
|
||||
|
||||
// DKIM domain that is higher-level than organizational can not result in a pass. ../rfc/7489:525
|
||||
test("example.com",
|
||||
[]dkim.Result{
|
||||
{
|
||||
Status: dkim.StatusPass,
|
||||
Sig: &dkim.Sig{ // Just the minimum fields needed.
|
||||
Domain: dns.Domain{ASCII: "com"},
|
||||
},
|
||||
Record: &dkim.Record{},
|
||||
},
|
||||
},
|
||||
spf.StatusNone,
|
||||
nil,
|
||||
true, Result{true, StatusFail, dns.Domain{ASCII: "example.com"}, &reject, nil},
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user