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