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 Dial: func(network, addr string) (net.Conn, error) {
186 if strings.HasPrefix(addr, "mta-sts.doesnotexist.example") {
187 return nil, &adns.DNSError{IsNotFound: true}
189 return net.Dial("tcp", l.Addr().String())
191 TLSClientConfig: &tls.Config{
196 HTTPClient.CloseIdleConnections()
197 HTTPClient.Transport = nil
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)
204 if err == nil && !reflect.DeepEqual(p, expPolicy) {
205 t.Fatalf("policy: got %#v, expected %#v", p, expPolicy)
208 if domain == "doesnotexist.example" {
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)
216 if err == nil && !reflect.DeepEqual(p, expPolicy) {
217 t.Fatalf("get: got %#v, expected %#v", p, expPolicy)
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)