1package mtasts
2
3import (
4 "context"
5 "crypto/ed25519"
6 cryptorand "crypto/rand"
7 "crypto/tls"
8 "crypto/x509"
9 "errors"
10 "io"
11 golog "log"
12 "math/big"
13 "net"
14 "net/http"
15 "reflect"
16 "strings"
17 "testing"
18 "time"
19
20 "github.com/mjl-/adns"
21
22 "github.com/mjl-/mox/dns"
23 "github.com/mjl-/mox/mlog"
24)
25
26func TestLookup(t *testing.T) {
27 log := mlog.New("mtasts", nil)
28
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"},
38 },
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.",
43 },
44 Fail: []string{
45 "txt _mta-sts.temperror.example.",
46 },
47 }
48
49 test := func(host string, expRecord *Record, expErr error) {
50 t.Helper()
51
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)
55 }
56 if err != nil {
57 return
58 }
59 if !reflect.DeepEqual(record, expRecord) {
60 t.Fatalf("lookup: got record %#v, expected %#v", record, expRecord)
61 }
62 }
63
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)
73}
74
75func TestMatches(t *testing.T) {
76 p, err := ParsePolicy("version: STSv1\nmode: enforce\nmax_age: 1\nmx: a.example\nmx: *.b.example\n")
77 if err != nil {
78 t.Fatalf("parsing policy: %s", err)
79 }
80
81 mustParseDomain := func(s string) dns.Domain {
82 t.Helper()
83 d, err := dns.ParseDomain(s)
84 if err != nil {
85 t.Fatalf("parsing domain %q: %s", s, err)
86 }
87 return d
88 }
89
90 match := func(s string) {
91 t.Helper()
92 if !p.Matches(mustParseDomain(s)) {
93 t.Fatalf("unexpected mismatch for %q", s)
94 }
95 }
96
97 not := func(s string) {
98 t.Helper()
99 if p.Matches(mustParseDomain(s)) {
100 t.Fatalf("unexpected match for %q", s)
101 }
102 }
103
104 match("a.example")
105 match("sub.b.example")
106 not("b.example")
107 not("sub.sub.b.example")
108 not("other")
109}
110
111func fakeCert(t *testing.T, expired bool) tls.Certificate {
112 notAfter := time.Now()
113 if expired {
114 notAfter = notAfter.Add(-time.Hour)
115 } else {
116 notAfter = notAfter.Add(time.Hour)
117 }
118
119 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
120
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),
125 NotAfter: notAfter,
126 }
127 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
128 if err != nil {
129 t.Fatalf("making certificate: %s", err)
130 }
131 cert, err := x509.ParseCertificate(localCertBuf)
132 if err != nil {
133 t.Fatalf("parsing generated certificate: %s", err)
134 }
135 c := tls.Certificate{
136 Certificate: [][]byte{localCertBuf},
137 PrivateKey: privKey,
138 Leaf: cert,
139 }
140 return c
141}
142
143func TestFetch(t *testing.T) {
144 log := mlog.New("mtasts", nil)
145
146 certok := fakeCert(t, false)
147 certbad := fakeCert(t, true)
148
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"},
153 },
154 }
155
156 test := func(cert tls.Certificate, domain string, status int, policyText string, expPolicy *Policy, expErr error) {
157 t.Helper()
158
159 pool := x509.NewCertPool()
160 pool.AddCert(cert.Leaf)
161
162 l, err := net.Listen("tcp", "127.0.0.1:0")
163 if err != nil {
164 t.Fatalf("listen: %v", err)
165 }
166 defer l.Close()
167 go func() {
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))
173 })
174 s := &http.Server{
175 Handler: mux,
176 TLSConfig: &tls.Config{
177 Certificates: []tls.Certificate{cert},
178 },
179 ErrorLog: golog.New(io.Discard, "", 0),
180 }
181 s.ServeTLS(l, "", "")
182 }()
183
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}
188 }
189 var dialer net.Dialer
190 return dialer.DialContext(ctx, "tcp", l.Addr().String())
191 },
192 TLSClientConfig: &tls.Config{
193 RootCAs: pool,
194 },
195 }
196 defer func() {
197 HTTPClient.CloseIdleConnections()
198 HTTPClient.Transport = nil
199 }()
200
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)
204 }
205 if err == nil && !reflect.DeepEqual(p, expPolicy) {
206 t.Fatalf("policy: got %#v, expected %#v", p, expPolicy)
207 }
208
209 if domain == "doesnotexist.example" {
210 expErr = ErrNoRecord
211 }
212
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)
216 }
217 if err == nil && !reflect.DeepEqual(p, expPolicy) {
218 t.Fatalf("get: got %#v, expected %#v", p, expPolicy)
219 }
220 }
221
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)
233}
234