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 Dial: func(network, addr string) (net.Conn, error) {
186 if strings.HasPrefix(addr, "mta-sts.doesnotexist.example") {
187 return nil, &adns.DNSError{IsNotFound: true}
188 }
189 return net.Dial("tcp", l.Addr().String())
190 },
191 TLSClientConfig: &tls.Config{
192 RootCAs: pool,
193 },
194 }
195 defer func() {
196 HTTPClient.CloseIdleConnections()
197 HTTPClient.Transport = nil
198 }()
199
200 p, _, err := FetchPolicy(context.Background(), log.Logger, dns.Domain{ASCII: domain})
201 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
202 t.Fatalf("policy: got err %#v, expected %#v", err, expErr)
203 }
204 if err == nil && !reflect.DeepEqual(p, expPolicy) {
205 t.Fatalf("policy: got %#v, expected %#v", p, expPolicy)
206 }
207
208 if domain == "doesnotexist.example" {
209 expErr = ErrNoRecord
210 }
211
212 _, p, _, err = Get(context.Background(), log.Logger, resolver, dns.Domain{ASCII: domain})
213 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
214 t.Fatalf("get: got err %#v, expected %#v", err, expErr)
215 }
216 if err == nil && !reflect.DeepEqual(p, expPolicy) {
217 t.Fatalf("get: got %#v, expected %#v", p, expPolicy)
218 }
219 }
220
221 test(certok, "mox.example", 200, "bogus", nil, ErrPolicySyntax)
222 test(certok, "other.example", 200, "bogus", nil, ErrPolicyFetch)
223 test(certbad, "mox.example", 200, "bogus", nil, ErrPolicyFetch)
224 test(certok, "mox.example", 404, "bogus", nil, ErrNoPolicy)
225 test(certok, "doesnotexist.example", 200, "bogus", nil, ErrNoPolicy)
226 test(certok, "mox.example", 301, "bogus", nil, ErrPolicyFetch)
227 test(certok, "mox.example", 500, "bogus", nil, ErrPolicyFetch)
228 large := make([]byte, 64*1024+2)
229 test(certok, "mox.example", 200, string(large), nil, ErrPolicySyntax)
230 validPolicy := "version:STSv1\nmode:none\nmax_age:1"
231 test(certok, "mox.example", 200, validPolicy, &Policy{Version: "STSv1", Mode: "none", MaxAgeSeconds: 1}, nil)
232}
233