1package tlsrpt
2
3import (
4 "context"
5 "crypto/ed25519"
6 cryptorand "crypto/rand"
7 "crypto/tls"
8 "crypto/x509"
9 "encoding/json"
10 "io"
11 "math/big"
12 "net"
13 "os"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/mjl-/mox/mlog"
19)
20
21var pkglog = mlog.New("tlsrpt", nil)
22
23const reportJSON = `{
24 "organization-name": "Company-X",
25 "date-range": {
26 "start-datetime": "2016-04-01T00:00:00Z",
27 "end-datetime": "2016-04-01T23:59:59Z"
28 },
29 "contact-info": "sts-reporting@company-x.example",
30 "report-id": "5065427c-23d3-47ca-b6e0-946ea0e8c4be",
31 "policies": [{
32 "policy": {
33 "policy-type": "sts",
34 "policy-string": ["version: STSv1","mode: testing",
35 "mx: *.mail.company-y.example","max_age: 86400"],
36 "policy-domain": "company-y.example",
37 "mx-host": ["*.mail.company-y.example"]
38 },
39 "summary": {
40 "total-successful-session-count": 5326,
41 "total-failure-session-count": 303
42 },
43 "failure-details": [{
44 "result-type": "certificate-expired",
45 "sending-mta-ip": "2001:db8:abcd:0012::1",
46 "receiving-mx-hostname": "mx1.mail.company-y.example",
47 "failed-session-count": 100
48 }, {
49 "result-type": "starttls-not-supported",
50 "sending-mta-ip": "2001:db8:abcd:0013::1",
51 "receiving-mx-hostname": "mx2.mail.company-y.example",
52 "receiving-ip": "203.0.113.56",
53 "failed-session-count": 200,
54 "additional-information": "https://reports.company-x.example/report_info ? id = 5065427 c - 23 d3# StarttlsNotSupported "
55 }, {
56 "result-type": "validation-failure",
57 "sending-mta-ip": "198.51.100.62",
58 "receiving-ip": "203.0.113.58",
59 "receiving-mx-hostname": "mx-backup.mail.company-y.example",
60 "failed-session-count": 3,
61 "failure-reason-code": "X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED"
62 }]
63 }]
64 }`
65
66// ../rfc/8460:1015
67var tlsrptMessage = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
68Date: Fri, May 09 2017 16:54:30 -0800
69To: mts-sts-tlsrpt@example.net
70Subject: Report Domain: example.net
71Submitter: mail.sender.example.com
72Report-ID: <735ff.e317+bf22029@example.net>
73TLS-Report-Domain: example.net
74TLS-Report-Submitter: mail.sender.example.com
75MIME-Version: 1.0
76Content-Type: multipart/report; report-type="tlsrpt";
77 boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
78Content-Language: en-us
79
80This is a multipart message in MIME format.
81
82------=_NextPart_000_024E_01CC9B0A.AFE54C00
83Content-Type: text/plain; charset="us-ascii"
84Content-Transfer-Encoding: 7bit
85
86This is an aggregate TLS report from mail.sender.example.com
87
88------=_NextPart_000_024E_01CC9B0A.AFE54C00
89Content-Type: application/tlsrpt+json
90Content-Transfer-Encoding: 8bit
91Content-Disposition: attachment;
92 filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
93
94`+reportJSON+`
95
96------=_NextPart_000_024E_01CC9B0A.AFE54C00--
97`, "\n", "\r\n")
98
99// Message without multipart.
100var tlsrptMessage2 = strings.ReplaceAll(`From: tlsrpt@mail.sender.example.com
101To: mts-sts-tlsrpt@example.net
102Subject: Report Domain: example.net
103Report-ID: <735ff.e317+bf22029@example.net>
104TLS-Report-Domain: example.net
105TLS-Report-Submitter: mail.sender.example.com
106MIME-Version: 1.0
107Content-Type: application/tlsrpt+json
108Content-Transfer-Encoding: 8bit
109Content-Disposition: attachment;
110 filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
111
112`+reportJSON+`
113`, "\n", "\r\n")
114
115func TestReport(t *testing.T) {
116 // ../rfc/8460:1756
117
118 var report ReportJSON
119 dec := json.NewDecoder(strings.NewReader(reportJSON))
120 dec.DisallowUnknownFields()
121 if err := dec.Decode(&report); err != nil {
122 t.Fatalf("parsing report: %s", err)
123 }
124
125 if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage)); err != nil {
126 t.Fatalf("parsing TLSRPT from message: %s", err)
127 }
128
129 if _, err := ParseMessage(pkglog.Logger, strings.NewReader(tlsrptMessage2)); err != nil {
130 t.Fatalf("parsing TLSRPT from message: %s", err)
131 }
132
133 if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "multipart/report", "multipart/related"))); err != ErrNoReport {
134 t.Fatalf("got err %v, expected ErrNoReport", err)
135 }
136
137 if _, err := ParseMessage(pkglog.Logger, strings.NewReader(strings.ReplaceAll(tlsrptMessage, "application/tlsrpt+json", "application/json"))); err != ErrNoReport {
138 t.Fatalf("got err %v, expected ErrNoReport", err)
139 }
140
141 files, err := os.ReadDir("../testdata/tlsreports")
142 if err != nil {
143 t.Fatalf("listing reports: %s", err)
144 }
145 for _, file := range files {
146 f, err := os.Open("../testdata/tlsreports/" + file.Name())
147 if err != nil {
148 t.Fatalf("open %q: %s", file, err)
149 }
150 if _, err := ParseMessage(pkglog.Logger, f); err != nil {
151 t.Fatalf("parsing TLSRPT from message %q: %s", file.Name(), err)
152 }
153 f.Close()
154 }
155}
156
157func TestTLSFailureDetails(t *testing.T) {
158 const alert70 = "tls-remote-alert-70-protocol-version-not-supported"
159
160 test := func(expResultType ResultType, expReasonCode string, client func(net.Conn) error, server func(net.Conn)) {
161 t.Helper()
162
163 cconn, sconn := net.Pipe()
164 defer cconn.Close()
165 defer sconn.Close()
166 go server(sconn)
167 err := client(cconn)
168 if err == nil {
169 t.Fatalf("expected tls error")
170 }
171
172 resultType, reasonCode := TLSFailureDetails(err)
173 if resultType != expResultType || !(reasonCode == expReasonCode || expReasonCode == alert70 && reasonCode == "tls-remote-alert-70") {
174 t.Fatalf("got %v %v, expected %v %v", resultType, reasonCode, expResultType, expReasonCode)
175 }
176 }
177
178 newPool := func(certs ...tls.Certificate) *x509.CertPool {
179 pool := x509.NewCertPool()
180 for _, cert := range certs {
181 pool.AddCert(cert.Leaf)
182 }
183 return pool
184 }
185
186 // Expired certificate.
187 expiredCert := fakeCert(t, "localhost", true)
188 test(ResultCertificateExpired, "",
189 func(conn net.Conn) error {
190 config := tls.Config{ServerName: "localhost", RootCAs: newPool(expiredCert)}
191 return tls.Client(conn, &config).Handshake()
192 },
193 func(conn net.Conn) {
194 config := tls.Config{Certificates: []tls.Certificate{expiredCert}}
195 tls.Server(conn, &config).Handshake()
196 },
197 )
198
199 // Hostname mismatch.
200 okCert := fakeCert(t, "localhost", false)
201 test(ResultCertificateHostMismatch, "", func(conn net.Conn) error {
202 config := tls.Config{ServerName: "otherhost", RootCAs: newPool(okCert)}
203 return tls.Client(conn, &config).Handshake()
204 },
205 func(conn net.Conn) {
206 config := tls.Config{Certificates: []tls.Certificate{okCert}}
207 tls.Server(conn, &config).Handshake()
208 },
209 )
210
211 // Not signed by trusted CA.
212 test(ResultCertificateNotTrusted, "", func(conn net.Conn) error {
213 config := tls.Config{ServerName: "localhost", RootCAs: newPool()}
214 return tls.Client(conn, &config).Handshake()
215 },
216 func(conn net.Conn) {
217 config := tls.Config{Certificates: []tls.Certificate{okCert}}
218 tls.Server(conn, &config).Handshake()
219 },
220 )
221
222 // We don't support the right protocol version.
223 test(ResultValidationFailure, alert70, func(conn net.Conn) error {
224 config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert), MinVersion: tls.VersionTLS10, MaxVersion: tls.VersionTLS10}
225 return tls.Client(conn, &config).Handshake()
226 },
227 func(conn net.Conn) {
228 config := tls.Config{Certificates: []tls.Certificate{okCert}, MinVersion: tls.VersionTLS12}
229 tls.Server(conn, &config).Handshake()
230 },
231 )
232
233 // todo: ideally a test for tls-local-alert-*
234
235 // Remote is not speaking TLS.
236 test(ResultValidationFailure, "tls-record-header-error", func(conn net.Conn) error {
237 config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
238 return tls.Client(conn, &config).Handshake()
239 },
240 func(conn net.Conn) {
241 go io.Copy(io.Discard, conn)
242 buf := make([]byte, 128)
243 for {
244 _, err := conn.Write(buf)
245 if err != nil {
246 break
247 }
248 }
249 },
250 )
251
252 // Context deadline exceeded during handshake.
253 test(ResultValidationFailure, "io-timeout-during-handshake",
254 func(conn net.Conn) error {
255 config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
256 ctx, cancel := context.WithTimeout(context.Background(), 1)
257 defer cancel()
258 return tls.Client(conn, &config).HandshakeContext(ctx)
259 },
260 func(conn net.Conn) {},
261 )
262
263 // Timeout during handshake.
264 test(ResultValidationFailure, "io-timeout-during-handshake",
265 func(conn net.Conn) error {
266 config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
267 conn.SetDeadline(time.Now())
268 return tls.Client(conn, &config).Handshake()
269 },
270 func(conn net.Conn) {},
271 )
272
273 // Closing connection during handshake.
274 test(ResultValidationFailure, "connection-closed-during-handshake", func(conn net.Conn) error {
275 config := tls.Config{ServerName: "localhost", RootCAs: newPool(okCert)}
276 return tls.Client(conn, &config).Handshake()
277 },
278 func(conn net.Conn) {
279 conn.Close()
280 },
281 )
282}
283
284// Just a cert that appears valid.
285func fakeCert(t *testing.T, name string, expired bool) tls.Certificate {
286 notAfter := time.Now()
287 if expired {
288 notAfter = notAfter.Add(-time.Hour)
289 } else {
290 notAfter = notAfter.Add(time.Hour)
291 }
292
293 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
294 template := &x509.Certificate{
295 SerialNumber: big.NewInt(1), // Required field...
296 DNSNames: []string{name},
297 NotBefore: time.Now().Add(-time.Hour),
298 NotAfter: notAfter,
299 }
300 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
301 if err != nil {
302 t.Fatalf("making certificate: %s", err)
303 }
304 cert, err := x509.ParseCertificate(localCertBuf)
305 if err != nil {
306 t.Fatalf("parsing generated certificate: %s", err)
307 }
308 c := tls.Certificate{
309 Certificate: [][]byte{localCertBuf},
310 PrivateKey: privKey,
311 Leaf: cert,
312 }
313 return c
314}
315
316func FuzzParseMessage(f *testing.F) {
317 f.Add(tlsrptMessage)
318 f.Fuzz(func(t *testing.T, s string) {
319 ParseMessage(pkglog.Logger, strings.NewReader(s))
320 })
321}
322