mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
276 lines
8.1 KiB
Go
276 lines
8.1 KiB
Go
|
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},
|
||
|
)
|
||
|
}
|