6 cryptorand "crypto/rand"
20 "github.com/mjl-/adns"
22 "github.com/mjl-/mox/dns"
23 "github.com/mjl-/mox/mlog"
26func TestLookup(t *testing.T) {
27 log := mlog.New("mtasts", nil)
29 resolver := dns.MockResolver{
30 TXT: map[string][]string{
31 "_mta-sts.a.example.": {"v=STSv1; id=1"},
32 "_mta-sts.one.example.": {"v=STSv1; id=1", "bogus"},
33 "_mta-sts.bad.example.": {"v=STSv1; bogus"},
34 "_mta-sts.multiple.example.": {"v=STSv1; id=1", "v=STSv1; id=2"},
35 "_mta-sts.c.cnames.example.": {"v=STSv1; id=1"},
36 "_mta-sts.temperror.example.": {"v=STSv1; id=1"},
37 "_mta-sts.other.example.": {"bogus", "more"},
39 CNAME: map[string]string{
40 "_mta-sts.a.cnames.example.": "_mta-sts.b.cnames.example.",
41 "_mta-sts.b.cnames.example.": "_mta-sts.c.cnames.example.",
42 "_mta-sts.followtemperror.example.": "_mta-sts.temperror.example.",
45 "txt _mta-sts.temperror.example.",
49 test := func(host string, expRecord *Record, expErr error) {
52 record, _, err := LookupRecord(context.Background(), log.Logger, resolver, dns.Domain{ASCII: host})
53 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
54 t.Fatalf("lookup: got err %#v, expected %#v", err, expErr)
59 if !reflect.DeepEqual(record, expRecord) {
60 t.Fatalf("lookup: got record %#v, expected %#v", record, expRecord)
64 test("absent.example", nil, ErrNoRecord)
65 test("other.example", nil, ErrNoRecord)
66 test("a.example", &Record{Version: "STSv1", ID: "1"}, nil)
67 test("one.example", &Record{Version: "STSv1", ID: "1"}, nil)
68 test("bad.example", nil, ErrRecordSyntax)
69 test("multiple.example", nil, ErrMultipleRecords)
70 test("a.cnames.example", &Record{Version: "STSv1", ID: "1"}, nil)
71 test("temperror.example", nil, ErrDNS)
72 test("followtemperror.example", nil, ErrDNS)
75func TestMatches(t *testing.T) {
76 p, err := ParsePolicy("version: STSv1\nmode: enforce\nmax_age: 1\nmx: a.example\nmx: *.b.example\n")
78 t.Fatalf("parsing policy: %s", err)
81 mustParseDomain := func(s string) dns.Domain {
83 d, err := dns.ParseDomain(s)
85 t.Fatalf("parsing domain %q: %s", s, err)
90 match := func(s string) {
92 if !p.Matches(mustParseDomain(s)) {
93 t.Fatalf("unexpected mismatch for %q", s)
97 not := func(s string) {
99 if p.Matches(mustParseDomain(s)) {
100 t.Fatalf("unexpected match for %q", s)
105 match("sub.b.example")
107 not("sub.sub.b.example")
111func fakeCert(t *testing.T, expired bool) tls.Certificate {
112 notAfter := time.Now()
114 notAfter = notAfter.Add(-time.Hour)
116 notAfter = notAfter.Add(time.Hour)
119 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
121 template := &x509.Certificate{
122 SerialNumber: big.NewInt(1), // Required field...
123 DNSNames: []string{"mta-sts.mox.example"},
124 NotBefore: time.Now().Add(-time.Hour),
127 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
129 t.Fatalf("making certificate: %s", err)
131 cert, err := x509.ParseCertificate(localCertBuf)
133 t.Fatalf("parsing generated certificate: %s", err)
135 c := tls.Certificate{
136 Certificate: [][]byte{localCertBuf},
143func TestFetch(t *testing.T) {
144 log := mlog.New("mtasts", nil)
146 certok := fakeCert(t, false)
147 certbad := fakeCert(t, true)
149 resolver := dns.MockResolver{
150 TXT: map[string][]string{
151 "_mta-sts.mox.example.": {"v=STSv1; id=1"},
152 "_mta-sts.other.example.": {"v=STSv1; id=1"},
156 test := func(cert tls.Certificate, domain string, status int, policyText string, expPolicy *Policy, expErr error) {
159 pool := x509.NewCertPool()
160 pool.AddCert(cert.Leaf)
162 l, err := net.Listen("tcp", "127.0.0.1:0")
164 t.Fatalf("listen: %v", err)
168 mux := &http.ServeMux{}
169 mux.HandleFunc("/.well-known/mta-sts.txt", func(w http.ResponseWriter, r *http.Request) {
170 w.Header().Add("Location", "/other") // Ignored except for redirect.
171 w.WriteHeader(status)
172 w.Write([]byte(policyText))
176 TLSConfig: &tls.Config{
177 Certificates: []tls.Certificate{cert},
179 ErrorLog: golog.New(io.Discard, "", 0),
181 s.ServeTLS(l, "", "")
184 HTTPClient.Transport = &http.Transport{
185 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
186 if strings.HasPrefix(addr, "mta-sts.doesnotexist.example") {
187 return nil, &adns.DNSError{IsNotFound: true}
189 var dialer net.Dialer
190 return dialer.DialContext(ctx, "tcp", l.Addr().String())
192 TLSClientConfig: &tls.Config{
197 HTTPClient.CloseIdleConnections()
198 HTTPClient.Transport = nil
201 p, _, err := FetchPolicy(context.Background(), log.Logger, dns.Domain{ASCII: domain})
202 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
203 t.Fatalf("policy: got err %#v, expected %#v", err, expErr)
205 if err == nil && !reflect.DeepEqual(p, expPolicy) {
206 t.Fatalf("policy: got %#v, expected %#v", p, expPolicy)
209 if domain == "doesnotexist.example" {
213 _, p, _, err = Get(context.Background(), log.Logger, resolver, dns.Domain{ASCII: domain})
214 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
215 t.Fatalf("get: got err %#v, expected %#v", err, expErr)
217 if err == nil && !reflect.DeepEqual(p, expPolicy) {
218 t.Fatalf("get: got %#v, expected %#v", p, expPolicy)
222 test(certok, "mox.example", 200, "bogus", nil, ErrPolicySyntax)
223 test(certok, "other.example", 200, "bogus", nil, ErrPolicyFetch)
224 test(certbad, "mox.example", 200, "bogus", nil, ErrPolicyFetch)
225 test(certok, "mox.example", 404, "bogus", nil, ErrNoPolicy)
226 test(certok, "doesnotexist.example", 200, "bogus", nil, ErrNoPolicy)
227 test(certok, "mox.example", 301, "bogus", nil, ErrPolicyFetch)
228 test(certok, "mox.example", 500, "bogus", nil, ErrPolicyFetch)
229 large := make([]byte, 64*1024+2)
230 test(certok, "mox.example", 200, string(large), nil, ErrPolicySyntax)
231 validPolicy := "version:STSv1\nmode:none\nmax_age:1"
232 test(certok, "mox.example", 200, validPolicy, &Policy{Version: "STSv1", Mode: "none", MaxAgeSeconds: 1}, nil)